1
1
from typing import (
2
2
TYPE_CHECKING ,
3
- Any ,
4
3
Dict ,
4
+ Generic ,
5
5
Iterable ,
6
6
List ,
7
7
Optional ,
8
8
Sequence ,
9
9
Tuple ,
10
+ TypeVar ,
10
11
Union ,
11
12
)
12
13
@@ -43,7 +44,11 @@ def retrieve_recommend_from_score(
43
44
raise ValueError ("Only float32 or float64 are allowed." )
44
45
45
46
46
- class IDMappedRecommender :
47
+ UserIdType = TypeVar ("UserIdType" )
48
+ ItemIdType = TypeVar ("ItemIdType" )
49
+
50
+
51
+ class IDMappedRecommender (Generic [UserIdType , ItemIdType ]):
47
52
"""A utility class that helps mapping user/item ids to index, retrieving recommendation score,
48
53
and making a recommendation.
49
54
@@ -63,7 +68,10 @@ class IDMappedRecommender:
63
68
"""
64
69
65
70
def __init__ (
66
- self , recommender : "BaseRecommender" , user_ids : List [Any ], item_ids : List [Any ]
71
+ self ,
72
+ recommender : "BaseRecommender" ,
73
+ user_ids : List [UserIdType ],
74
+ item_ids : List [ItemIdType ],
67
75
):
68
76
69
77
if (recommender .n_users != len (user_ids )) or (
@@ -79,11 +87,11 @@ def __init__(
79
87
self .user_id_to_index = {user_id : i for i , user_id in enumerate (user_ids )}
80
88
self .item_id_to_index = {item_id : i for i , item_id in enumerate (item_ids )}
81
89
82
- def _item_id_list_to_index_list (self , ids : Iterable [Any ]) -> List [int ]:
90
+ def _item_id_list_to_index_list (self , ids : Iterable [ItemIdType ]) -> List [int ]:
83
91
return [self .item_id_to_index [id ] for id in ids if id in self .item_id_to_index ]
84
92
85
93
def _user_profile_to_data_col (
86
- self , profile : Union [List [Any ], Dict [Any , float ]]
94
+ self , profile : Union [List [ItemIdType ], Dict [ItemIdType , float ]]
87
95
) -> Tuple [List [float ], List [int ]]:
88
96
data : List [float ]
89
97
cols : List [int ]
@@ -101,7 +109,7 @@ def _user_profile_to_data_col(
101
109
return data , cols
102
110
103
111
def _list_of_user_profile_to_matrix (
104
- self , users_info : Sequence [Union [List [Any ], Dict [Any , float ]]]
112
+ self , users_info : Sequence [Union [List [ItemIdType ], Dict [ItemIdType , float ]]]
105
113
) -> sps .csr_matrix :
106
114
data : List [float ] = []
107
115
indptr : List [int ] = [0 ]
@@ -120,11 +128,11 @@ def _list_of_user_profile_to_matrix(
120
128
121
129
def get_recommendation_for_known_user_id (
122
130
self ,
123
- user_id : Any ,
131
+ user_id : UserIdType ,
124
132
cutoff : int = 20 ,
125
- allowed_item_ids : Optional [List [Any ]] = None ,
126
- forbidden_item_ids : Optional [List [Any ]] = None ,
127
- ) -> List [Tuple [Any , float ]]:
133
+ allowed_item_ids : Optional [List [ItemIdType ]] = None ,
134
+ forbidden_item_ids : Optional [List [ItemIdType ]] = None ,
135
+ ) -> List [Tuple [ItemIdType , float ]]:
128
136
"""Retrieve recommendation result for a known user.
129
137
Args:
130
138
user_id:
@@ -151,7 +159,7 @@ def get_recommendation_for_known_user_id(
151
159
)
152
160
153
161
score = self .recommender .get_score_remove_seen (user_index )[0 , :]
154
- return self ._score_to_recommended_items (
162
+ return self .score_to_recommended_items (
155
163
score ,
156
164
cutoff = cutoff ,
157
165
allowed_item_ids = allowed_item_ids ,
@@ -160,12 +168,13 @@ def get_recommendation_for_known_user_id(
160
168
161
169
def get_recommendation_for_known_user_batch (
162
170
self ,
163
- user_ids : List [Any ],
171
+ user_ids : List [UserIdType ],
164
172
cutoff : int = 20 ,
165
- allowed_item_ids : Optional [List [List [Any ]]] = None ,
166
- forbidden_item_ids : Optional [List [List [Any ]]] = None ,
173
+ allowed_item_ids : Optional [List [ItemIdType ]] = None ,
174
+ per_user_allowed_item_ids : Optional [List [List [ItemIdType ]]] = None ,
175
+ forbidden_item_ids : Optional [List [List [ItemIdType ]]] = None ,
167
176
n_threads : Optional [int ] = None ,
168
- ) -> List [List [Tuple [Any , float ]]]:
177
+ ) -> List [List [Tuple [ItemIdType , float ]]]:
169
178
"""Retrieve recommendation result for a list of known users.
170
179
171
180
Args:
@@ -174,13 +183,21 @@ def get_recommendation_for_known_user_batch(
174
183
cutoff:
175
184
Maximal number of recommendations allowed.
176
185
allowed_item_ids:
186
+ If not ``None``, defines "a list of recommendable item IDs".
187
+ Ignored if `per_user_allowed_item_ids` is set.
188
+ per_user_allowed_item_ids:
177
189
If not ``None``, defines "a list of list of recommendable item IDs"
178
- and ``len(allowed_item_ids)`` must be equal to ``len(item_ids) ``.
190
+ and ``len(allowed_item_ids)`` must be equal to ``score.shape[0] ``.
179
191
Defaults to ``None``.
192
+
180
193
forbidden_item_ids:
181
194
If not ``None``, defines "a list of list of forbidden item IDs"
182
195
and ``len(allowed_item_ids)`` must be equal to ``len(item_ids)``
183
196
Defaults to ``None``.
197
+ n_threads:
198
+ Specifies the number of threads to use for the computation.
199
+ If ``None``, the environment variable ``"IRSPACK_NUM_THREADS_DEFAULT"`` will be looked up,
200
+ and if the variable is not set, it will be set to ``os.cpu_count()``. Defaults to None.
184
201
185
202
Returns:
186
203
A list of list of tuples consisting of ``(item_id, score)``.
@@ -191,21 +208,22 @@ def get_recommendation_for_known_user_batch(
191
208
)
192
209
193
210
score = self .recommender .get_score_remove_seen (user_indexes )
194
- return self ._score_to_recommended_items_batch (
211
+ return self .score_to_recommended_items_batch (
195
212
score ,
196
213
cutoff = cutoff ,
197
214
allowed_item_ids = allowed_item_ids ,
215
+ per_user_allowed_item_ids = per_user_allowed_item_ids ,
198
216
forbidden_item_ids = forbidden_item_ids ,
199
217
n_threads = get_n_threads (n_threads = n_threads ),
200
218
)
201
219
202
220
def get_recommendation_for_new_user (
203
221
self ,
204
- user_profile : Union [List [Any ], Dict [Any , float ]],
222
+ user_profile : Union [List [ItemIdType ], Dict [ItemIdType , float ]],
205
223
cutoff : int = 20 ,
206
- allowed_item_ids : Optional [List [Any ]] = None ,
207
- forbidden_item_ids : Optional [List [Any ]] = None ,
208
- ) -> List [Tuple [Any , float ]]:
224
+ allowed_item_ids : Optional [List [ItemIdType ]] = None ,
225
+ forbidden_item_ids : Optional [List [ItemIdType ]] = None ,
226
+ ) -> List [Tuple [ItemIdType , float ]]:
209
227
"""Retrieve recommendation result for a previously unseen user using item ids with which he or she interacted.
210
228
211
229
Args:
@@ -229,7 +247,7 @@ def get_recommendation_for_new_user(
229
247
(data , cols , [0 , len (cols )]), shape = (1 , len (self .item_ids ))
230
248
)
231
249
score = self .recommender .get_score_cold_user_remove_seen (X_input )[0 ]
232
- return self ._score_to_recommended_items (
250
+ return self .score_to_recommended_items (
233
251
score ,
234
252
cutoff ,
235
253
allowed_item_ids = allowed_item_ids ,
@@ -238,12 +256,13 @@ def get_recommendation_for_new_user(
238
256
239
257
def get_recommendation_for_new_user_batch (
240
258
self ,
241
- user_profiles : Sequence [Union [List [Any ], Dict [Any , float ]]],
259
+ user_profiles : Sequence [Union [List [ItemIdType ], Dict [ItemIdType , float ]]],
242
260
cutoff : int = 20 ,
243
- allowed_item_ids : Optional [List [List [Any ]]] = None ,
244
- forbidden_item_ids : Optional [List [List [Any ]]] = None ,
261
+ allowed_item_ids : Optional [List [ItemIdType ]] = None ,
262
+ per_user_allowed_item_ids : Optional [List [List [ItemIdType ]]] = None ,
263
+ forbidden_item_ids : Optional [List [List [ItemIdType ]]] = None ,
245
264
n_threads : Optional [int ] = None ,
246
- ) -> List [List [Tuple [Any , float ]]]:
265
+ ) -> List [List [Tuple [ItemIdType , float ]]]:
247
266
"""Retrieve recommendation result for a previously unseen users using item ids with which they have interacted.
248
267
249
268
Args:
@@ -254,35 +273,43 @@ def get_recommendation_for_new_user_batch(
254
273
cutoff:
255
274
Maximal number of recommendations allowed.
256
275
allowed_item_ids:
276
+ If not ``None``, defines "a list of recommendable item IDs".
277
+ Ignored if `per_user_allowed_item_ids` is set.
278
+ per_user_allowed_item_ids:
257
279
If not ``None``, defines "a list of list of recommendable item IDs"
258
- and ``len(allowed_item_ids)`` must be equal to ``len(item_ids) ``.
280
+ and ``len(allowed_item_ids)`` must be equal to ``score.shape[0] ``.
259
281
Defaults to ``None``.
260
282
forbidden_item_ids:
261
283
If not ``None``, defines "a list of list of forbidden item IDs"
262
284
and ``len(allowed_item_ids)`` must be equal to ``len(item_ids)``
263
285
Defaults to ``None``.
286
+ n_threads:
287
+ Specifies the number of threads to use for the computation.
288
+ If ``None``, the environment variable ``"IRSPACK_NUM_THREADS_DEFAULT"`` will be looked up,
289
+ and if the variable is not set, it will be set to ``os.cpu_count()``. Defaults to None.
264
290
265
291
Returns:
266
292
A list of list of tuples consisting of ``(item_id, score)``.
267
293
Each internal list corresponds to the recommender's recommendation output.
268
294
"""
269
295
X_input = self ._list_of_user_profile_to_matrix (user_profiles )
270
296
score = self .recommender .get_score_cold_user_remove_seen (X_input )
271
- return self ._score_to_recommended_items_batch (
297
+ return self .score_to_recommended_items_batch (
272
298
score ,
273
299
cutoff ,
274
300
allowed_item_ids = allowed_item_ids ,
301
+ per_user_allowed_item_ids = per_user_allowed_item_ids ,
275
302
forbidden_item_ids = forbidden_item_ids ,
276
303
n_threads = get_n_threads (n_threads = n_threads ),
277
304
)
278
305
279
- def _score_to_recommended_items (
306
+ def score_to_recommended_items (
280
307
self ,
281
308
score : DenseScoreArray ,
282
309
cutoff : int ,
283
- allowed_item_ids : Optional [List [Any ]] = None ,
284
- forbidden_item_ids : Optional [List [Any ]] = None ,
285
- ) -> List [Tuple [Any , float ]]:
310
+ allowed_item_ids : Optional [List [ItemIdType ]] = None ,
311
+ forbidden_item_ids : Optional [List [ItemIdType ]] = None ,
312
+ ) -> List [Tuple [ItemIdType , float ]]:
286
313
if allowed_item_ids is not None :
287
314
allowed_item_indices = np .asarray (
288
315
self ._item_id_list_to_index_list (allowed_item_ids ), dtype = np .int64
@@ -292,7 +319,7 @@ def _score_to_recommended_items(
292
319
]
293
320
else :
294
321
high_score_inds = score .argsort ()[::- 1 ]
295
- recommendations : List [Tuple [Any , float ]] = []
322
+ recommendations : List [Tuple [ItemIdType , float ]] = []
296
323
for i in high_score_inds :
297
324
i_int = int (i )
298
325
score_this = score [i_int ]
@@ -307,24 +334,51 @@ def _score_to_recommended_items(
307
334
break
308
335
return recommendations
309
336
310
- def _score_to_recommended_items_batch (
337
+ def score_to_recommended_items_batch (
311
338
self ,
312
339
score : DenseScoreArray ,
313
340
cutoff : int ,
314
- allowed_item_ids : Optional [List [List [Any ]]] = None ,
315
- forbidden_item_ids : Optional [List [List [Any ]]] = None ,
316
- n_threads : int = 1 ,
317
- ) -> List [List [Tuple [Any , float ]]]:
341
+ allowed_item_ids : Optional [List [ItemIdType ]] = None ,
342
+ per_user_allowed_item_ids : Optional [List [List [ItemIdType ]]] = None ,
343
+ forbidden_item_ids : Optional [List [List [ItemIdType ]]] = None ,
344
+ n_threads : Optional [int ] = None ,
345
+ ) -> List [List [Tuple [ItemIdType , float ]]]:
346
+ r"""Retrieve recommendation from score array.
347
+ Args:
348
+ score:
349
+ 1d numpy ndarray for score.
350
+ cutoff:
351
+ Maximal number of recommendations allowed.
352
+ allowed_item_ids:
353
+ If not ``None``, defines "a list of recommendable item IDs".
354
+ Ignored if `per_user_allowed_item_ids` is set.
355
+ per_user_allowed_item_ids:
356
+ If not ``None``, defines "a list of list of recommendable item IDs"
357
+ and ``len(allowed_item_ids)`` must be equal to ``score.shape[0]``.
358
+ Defaults to ``None``.
359
+ allowed_item_ids:
360
+ If not ``None``, defines "a list of list of recommendable item IDs"
361
+ and ``len(allowed_item_ids)`` must be equal to ``len(item_ids)``.
362
+ Defaults to ``None``.
363
+ forbidden_item_ids:
364
+ If not ``None``, defines "a list of list of forbidden item IDs"
365
+ and ``len(allowed_item_ids)`` must be equal to ``len(item_ids)``
366
+ Defaults to ``None``.
367
+
368
+ """
369
+
318
370
if forbidden_item_ids is not None :
319
371
assert len (forbidden_item_ids ) == score .shape [0 ]
320
- if allowed_item_ids is not None :
321
- assert len (allowed_item_ids ) == score .shape [0 ]
372
+ if per_user_allowed_item_ids is not None :
373
+ assert len (per_user_allowed_item_ids ) == score .shape [0 ]
322
374
323
375
allowed_item_indices : List [List [int ]] = []
324
- if allowed_item_ids is not None :
376
+ if per_user_allowed_item_ids is not None :
325
377
allowed_item_indices = [
326
- self ._item_id_list_to_index_list (_ ) for _ in allowed_item_ids
378
+ self ._item_id_list_to_index_list (_ ) for _ in per_user_allowed_item_ids
327
379
]
380
+ elif allowed_item_ids is not None :
381
+ allowed_item_indices = [self ._item_id_list_to_index_list (allowed_item_ids )]
328
382
if forbidden_item_ids is not None :
329
383
for u , forbidden_ids_per_user in enumerate (forbidden_item_ids ):
330
384
score [
@@ -335,7 +389,7 @@ def _score_to_recommended_items_batch(
335
389
score ,
336
390
allowed_item_indices ,
337
391
cutoff ,
338
- n_threads = n_threads ,
392
+ n_threads = get_n_threads ( n_threads ) ,
339
393
)
340
394
return [
341
395
[
0 commit comments