replay-rec 0.20.3__py3-none-any.whl → 0.20.3rc0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. replay/__init__.py +1 -1
  2. replay/experimental/__init__.py +0 -0
  3. replay/experimental/metrics/__init__.py +62 -0
  4. replay/experimental/metrics/base_metric.py +603 -0
  5. replay/experimental/metrics/coverage.py +97 -0
  6. replay/experimental/metrics/experiment.py +175 -0
  7. replay/experimental/metrics/hitrate.py +26 -0
  8. replay/experimental/metrics/map.py +30 -0
  9. replay/experimental/metrics/mrr.py +18 -0
  10. replay/experimental/metrics/ncis_precision.py +31 -0
  11. replay/experimental/metrics/ndcg.py +49 -0
  12. replay/experimental/metrics/precision.py +22 -0
  13. replay/experimental/metrics/recall.py +25 -0
  14. replay/experimental/metrics/rocauc.py +49 -0
  15. replay/experimental/metrics/surprisal.py +90 -0
  16. replay/experimental/metrics/unexpectedness.py +76 -0
  17. replay/experimental/models/__init__.py +50 -0
  18. replay/experimental/models/admm_slim.py +257 -0
  19. replay/experimental/models/base_neighbour_rec.py +200 -0
  20. replay/experimental/models/base_rec.py +1386 -0
  21. replay/experimental/models/base_torch_rec.py +234 -0
  22. replay/experimental/models/cql.py +454 -0
  23. replay/experimental/models/ddpg.py +932 -0
  24. replay/experimental/models/dt4rec/__init__.py +0 -0
  25. replay/experimental/models/dt4rec/dt4rec.py +189 -0
  26. replay/experimental/models/dt4rec/gpt1.py +401 -0
  27. replay/experimental/models/dt4rec/trainer.py +127 -0
  28. replay/experimental/models/dt4rec/utils.py +264 -0
  29. replay/experimental/models/extensions/spark_custom_models/__init__.py +0 -0
  30. replay/experimental/models/extensions/spark_custom_models/als_extension.py +792 -0
  31. replay/experimental/models/hierarchical_recommender.py +331 -0
  32. replay/experimental/models/implicit_wrap.py +131 -0
  33. replay/experimental/models/lightfm_wrap.py +303 -0
  34. replay/experimental/models/mult_vae.py +332 -0
  35. replay/experimental/models/neural_ts.py +986 -0
  36. replay/experimental/models/neuromf.py +406 -0
  37. replay/experimental/models/scala_als.py +293 -0
  38. replay/experimental/models/u_lin_ucb.py +115 -0
  39. replay/experimental/nn/data/__init__.py +1 -0
  40. replay/experimental/nn/data/schema_builder.py +102 -0
  41. replay/experimental/preprocessing/__init__.py +3 -0
  42. replay/experimental/preprocessing/data_preparator.py +839 -0
  43. replay/experimental/preprocessing/padder.py +229 -0
  44. replay/experimental/preprocessing/sequence_generator.py +208 -0
  45. replay/experimental/scenarios/__init__.py +1 -0
  46. replay/experimental/scenarios/obp_wrapper/__init__.py +8 -0
  47. replay/experimental/scenarios/obp_wrapper/obp_optuna_objective.py +74 -0
  48. replay/experimental/scenarios/obp_wrapper/replay_offline.py +261 -0
  49. replay/experimental/scenarios/obp_wrapper/utils.py +85 -0
  50. replay/experimental/scenarios/two_stages/__init__.py +0 -0
  51. replay/experimental/scenarios/two_stages/reranker.py +117 -0
  52. replay/experimental/scenarios/two_stages/two_stages_scenario.py +757 -0
  53. replay/experimental/utils/__init__.py +0 -0
  54. replay/experimental/utils/logger.py +24 -0
  55. replay/experimental/utils/model_handler.py +186 -0
  56. replay/experimental/utils/session_handler.py +44 -0
  57. {replay_rec-0.20.3.dist-info → replay_rec-0.20.3rc0.dist-info}/METADATA +11 -17
  58. {replay_rec-0.20.3.dist-info → replay_rec-0.20.3rc0.dist-info}/RECORD +61 -6
  59. {replay_rec-0.20.3.dist-info → replay_rec-0.20.3rc0.dist-info}/WHEEL +0 -0
  60. {replay_rec-0.20.3.dist-info → replay_rec-0.20.3rc0.dist-info}/licenses/LICENSE +0 -0
  61. {replay_rec-0.20.3.dist-info → replay_rec-0.20.3rc0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,261 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from typing import Any, Optional, Union
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from obp.policy.base import BaseOfflinePolicyLearner
8
+ from optuna import create_study
9
+ from optuna.samplers import TPESampler
10
+ from pyspark.sql import DataFrame
11
+
12
+ from replay.data import Dataset, FeatureHint, FeatureInfo, FeatureSchema, FeatureType
13
+ from replay.experimental.models.base_rec import BaseRecommender as ExperimentalBaseRecommender
14
+ from replay.experimental.scenarios.obp_wrapper.obp_optuna_objective import OBPObjective
15
+ from replay.experimental.scenarios.obp_wrapper.utils import split_bandit_feedback
16
+ from replay.models.base_rec import BaseRecommender
17
+ from replay.utils.spark_utils import convert2spark
18
+
19
+
20
+ def obp2df(
21
+ action: np.ndarray, reward: np.ndarray, timestamp: np.ndarray, feedback_column: str
22
+ ) -> Optional[pd.DataFrame]:
23
+ """
24
+ Converts OBP log to the pandas DataFrame
25
+ """
26
+
27
+ n_interactions = len(action)
28
+
29
+ df = pd.DataFrame(
30
+ {
31
+ "user_idx": np.arange(n_interactions),
32
+ "item_idx": action,
33
+ feedback_column: reward,
34
+ "timestamp": timestamp,
35
+ }
36
+ )
37
+
38
+ return df
39
+
40
+
41
+ def context2df(context: np.ndarray, idx_col: np.ndarray, idx_col_name: str) -> Optional[pd.DataFrame]:
42
+ """
43
+ Converts OBP log to the pandas DataFrame
44
+ """
45
+
46
+ df1 = pd.DataFrame({idx_col_name + "_idx": idx_col})
47
+ cols = [str(i) + "_" + idx_col_name for i in range(context.shape[1])]
48
+ df2 = pd.DataFrame(context, columns=cols)
49
+
50
+ return df1.join(df2)
51
+
52
+
53
+ @dataclass
54
+ class OBPOfflinePolicyLearner(BaseOfflinePolicyLearner):
55
+ """
56
+ Off-policy learner which wraps OBP data representation into replay format.
57
+
58
+ :param n_actions: Number of actions.
59
+
60
+ :param len_list: Length of a list of actions in a recommendation/ranking inferface,
61
+ slate size. When Open Bandit Dataset is used, 3 should be set.
62
+
63
+ :param replay_model: Any model from replay library with fit, predict functions.
64
+
65
+ :param dataset: Dataset of interactions (user_id, item_id, rating).
66
+ Constructing inside the fit method. Used for predict of replay_model.
67
+ """
68
+
69
+ replay_model: Optional[Union[BaseRecommender, ExperimentalBaseRecommender]] = None
70
+ log: Optional[DataFrame] = None
71
+ max_usr_id: int = 0
72
+ item_features: DataFrame = None
73
+ _study = None
74
+ _logger: Optional[logging.Logger] = None
75
+ _objective = OBPObjective
76
+
77
+ def __post_init__(self) -> None:
78
+ """Initialize Class."""
79
+
80
+ self.is_experimental_model = isinstance(self.replay_model, ExperimentalBaseRecommender)
81
+ if self.is_experimental_model:
82
+ self.feedback_column = "relevance"
83
+ else:
84
+ self.feedback_column = "rating"
85
+
86
+ self.feature_schema = FeatureSchema(
87
+ [
88
+ FeatureInfo(
89
+ column="user_idx",
90
+ feature_type=FeatureType.CATEGORICAL,
91
+ feature_hint=FeatureHint.QUERY_ID,
92
+ ),
93
+ FeatureInfo(
94
+ column="item_idx",
95
+ feature_type=FeatureType.CATEGORICAL,
96
+ feature_hint=FeatureHint.ITEM_ID,
97
+ ),
98
+ FeatureInfo(
99
+ column="rating",
100
+ feature_type=FeatureType.NUMERICAL,
101
+ feature_hint=FeatureHint.RATING,
102
+ ),
103
+ FeatureInfo(
104
+ column="timestamp",
105
+ feature_type=FeatureType.NUMERICAL,
106
+ feature_hint=FeatureHint.TIMESTAMP,
107
+ ),
108
+ ]
109
+ )
110
+
111
+ @property
112
+ def logger(self) -> logging.Logger:
113
+ """
114
+ :return: get library logger
115
+ """
116
+ if self._logger is None:
117
+ self._logger = logging.getLogger("replay")
118
+ return self._logger
119
+
120
+ def fit(
121
+ self,
122
+ action: np.ndarray,
123
+ reward: np.ndarray,
124
+ timestamp: np.ndarray,
125
+ context: np.ndarray = None,
126
+ action_context: np.ndarray = None,
127
+ ) -> None:
128
+ """
129
+ Fits an offline bandit policy on the given logged bandit data.
130
+ This `fit` method wraps bandit data and calls `fit` method for the replay_model.
131
+
132
+ :param action: Actions sampled by the logging/behavior policy
133
+ for each data in logged bandit data, i.e., :math:`a_i`.
134
+
135
+ :param reward: Rewards observed for each data in logged bandit data, i.e., :math:`r_i`.
136
+
137
+ :param timestamp: Moment of time when user interacted with corresponding item.
138
+
139
+ :param context: Context vectors observed for each data, i.e., :math:`x_i`.
140
+
141
+ :param action_context: Context vectors observed for each action.
142
+ """
143
+
144
+ log = convert2spark(obp2df(action, reward, timestamp, self.feedback_column))
145
+ self.log = log
146
+
147
+ user_features = None
148
+ self.max_usr_id = reward.shape[0]
149
+
150
+ if context is not None:
151
+ user_features = convert2spark(context2df(context, np.arange(context.shape[0]), "user"))
152
+
153
+ if action_context is not None:
154
+ self.item_features = convert2spark(context2df(action_context, np.arange(self.n_actions), "item"))
155
+
156
+ if self.is_experimental_model:
157
+ self.replay_model._fit_wrap(log, user_features, self.item_features)
158
+ else:
159
+ dataset = Dataset(
160
+ feature_schema=self.feature_schema,
161
+ interactions=log,
162
+ query_features=user_features,
163
+ item_features=self.item_features,
164
+ )
165
+ self.replay_model._fit_wrap(dataset)
166
+
167
+ def predict(self, n_rounds: int = 1, context: np.ndarray = None) -> np.ndarray:
168
+ """Predict best actions for new data.
169
+ Action set predicted by this `predict` method can contain duplicate items.
170
+ If a non-repetitive action set is needed, please use the `sample_action` method.
171
+
172
+ :context: Context vectors for new data.
173
+
174
+ :return: Action choices made by a classifier, which can contain duplicate items.
175
+ If a non-repetitive action set is needed, please use the `sample_action` method.
176
+ """
177
+
178
+ user_features = None
179
+ if context is not None:
180
+ user_features = convert2spark(
181
+ context2df(context, np.arange(self.max_usr_id, self.max_usr_id + n_rounds), "user")
182
+ )
183
+
184
+ users = convert2spark(pd.DataFrame({"user_idx": np.arange(self.max_usr_id, self.max_usr_id + n_rounds)}))
185
+ items = convert2spark(pd.DataFrame({"item_idx": np.arange(self.n_actions)}))
186
+
187
+ self.max_usr_id += n_rounds
188
+
189
+ if self.is_experimental_model:
190
+ action_dist = self.replay_model._predict_proba(
191
+ self.log, self.len_list, users, items, user_features, self.item_features, filter_seen_items=False
192
+ )
193
+ else:
194
+ dataset = Dataset(
195
+ feature_schema=self.feature_schema,
196
+ interactions=self.log,
197
+ query_features=user_features,
198
+ item_features=self.item_features,
199
+ check_consistency=False,
200
+ )
201
+ action_dist = self.replay_model._predict_proba(
202
+ dataset, self.len_list, users, items, filter_seen_items=False
203
+ )
204
+ return action_dist
205
+
206
+ def optimize(
207
+ self,
208
+ bandit_feedback: dict[str, np.ndarray],
209
+ val_size: float = 0.3,
210
+ param_borders: Optional[dict[str, list[Any]]] = None,
211
+ criterion: str = "ipw",
212
+ budget: int = 10,
213
+ new_study: bool = True,
214
+ ) -> Optional[dict[str, Any]]:
215
+ """Optimize model parameters using optuna.
216
+ Optimization is carried out over the IPW/DR/DM scores(IPW by default).
217
+
218
+ :param bandit_feedback: Bandit log data with fields
219
+ ``[action, reward, context, action_context,
220
+ n_rounds, n_actions, position, pscore]`` as in OpenBanditPipeline.
221
+
222
+ :param val_size: Size of validation subset.
223
+
224
+ :param param_borders: Dictionary of parameter names with pair of borders
225
+ for the parameters optimization algorithm.
226
+
227
+ :param criterion: Score for optimization. Available are `ipw`, `dr` and `dm`.
228
+
229
+ :param budget: Number of trials for the optimization algorithm.
230
+
231
+ :param new_study: Flag to create new study or not for optuna.
232
+
233
+ :return: Dictionary of parameter names with optimal value of corresponding parameter.
234
+ """
235
+
236
+ bandit_feedback_train, bandit_feedback_val = split_bandit_feedback(bandit_feedback, val_size)
237
+
238
+ if self.replay_model._search_space is None:
239
+ self.logger.warning("%s has no hyper parameters to optimize", str(self))
240
+ return None
241
+
242
+ if self._study is None or new_study:
243
+ self._study = create_study(direction="maximize", sampler=TPESampler())
244
+
245
+ search_space = self.replay_model._prepare_param_borders(param_borders)
246
+ if self.replay_model._init_params_in_search_space(search_space) and not self.replay_model._params_tried():
247
+ self._study.enqueue_trial(self.replay_model._init_args)
248
+
249
+ objective = self._objective(
250
+ search_space=search_space,
251
+ bandit_feedback_train=bandit_feedback_train,
252
+ bandit_feedback_val=bandit_feedback_val,
253
+ learner=self,
254
+ criterion=criterion,
255
+ k=self.len_list,
256
+ )
257
+
258
+ self._study.optimize(objective, budget)
259
+ best_params = self._study.best_params
260
+ self.replay_model.set_params(**best_params)
261
+ return best_params
@@ -0,0 +1,85 @@
1
+ import numpy as np
2
+ from obp.ope import RegressionModel
3
+ from sklearn.linear_model import LogisticRegression
4
+
5
+
6
+ def get_est_rewards_by_reg(n_actions, len_list, bandit_feedback_train, bandit_feedback_test):
7
+ """
8
+ Fit Logistic Regression to rewards from `bandit_feedback`.
9
+ """
10
+ regression_model = RegressionModel(
11
+ n_actions=n_actions,
12
+ len_list=len_list,
13
+ action_context=bandit_feedback_train["action_context"],
14
+ base_model=LogisticRegression(max_iter=1000, random_state=12345),
15
+ )
16
+
17
+ regression_model.fit(
18
+ context=bandit_feedback_train["context"],
19
+ action=bandit_feedback_train["action"],
20
+ reward=bandit_feedback_train["reward"],
21
+ position=bandit_feedback_train["position"],
22
+ pscore=bandit_feedback_train["pscore"],
23
+ )
24
+
25
+ estimated_rewards_by_reg_model = regression_model.predict(
26
+ context=bandit_feedback_test["context"],
27
+ )
28
+
29
+ return estimated_rewards_by_reg_model
30
+
31
+
32
+ def bandit_subset(borders: list[int], bandit_feedback: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
33
+ """
34
+ This function returns subset of a `bandit_feedback`
35
+ with borders specified in `borders`.
36
+
37
+ :param bandit_feedback: Bandit log data with fields
38
+ ``[action, reward, context, action_context,
39
+ n_rounds, n_actions, position, pscore]``
40
+ as in OpenBanditPipeline.
41
+ :param borders: List with two values ``[left, right]``
42
+ :return: Returns subset of a `bandit_feedback` for each key with
43
+ indexes from `left`(including) to `right`(excluding).
44
+ """
45
+ assert len(borders) == 2
46
+
47
+ left, right = borders
48
+
49
+ assert left < right
50
+
51
+ position = None if bandit_feedback["position"] is None else bandit_feedback["position"][left:right]
52
+
53
+ return {
54
+ "n_rounds": right - left,
55
+ "n_actions": bandit_feedback["n_actions"],
56
+ "action": bandit_feedback["action"][left:right],
57
+ "position": position,
58
+ "reward": bandit_feedback["reward"][left:right],
59
+ "pscore": bandit_feedback["pscore"][left:right],
60
+ "context": bandit_feedback["context"][left:right],
61
+ "action_context": bandit_feedback["action_context"][left:right],
62
+ }
63
+
64
+
65
+ def split_bandit_feedback(
66
+ bandit_feedback: dict[str, np.ndarray], val_size: int = 0.3
67
+ ) -> tuple[dict[str, np.ndarray], dict[str, np.ndarray]]:
68
+ """
69
+ Split `bandit_feedback` into two subsets.
70
+ :param bandit_feedback: Bandit log data with fields
71
+ ``[action, reward, context, action_context,
72
+ n_rounds, n_actions, position, pscore]``
73
+ as in OpenBanditPipeline.
74
+ :param val_size: Number in range ``[0, 1]`` corresponding to the proportion of
75
+ train/val split.
76
+ :return: `bandit_feedback_train` and `bandit_feedback_val` split.
77
+ """
78
+
79
+ n_rounds = bandit_feedback["n_rounds"]
80
+ n_rounds_train = int(n_rounds * (1.0 - val_size))
81
+
82
+ bandit_feedback_train = bandit_subset([0, n_rounds_train], bandit_feedback)
83
+ bandit_feedback_val = bandit_subset([n_rounds_train, n_rounds], bandit_feedback)
84
+
85
+ return bandit_feedback_train, bandit_feedback_val
File without changes
@@ -0,0 +1,117 @@
1
+ import logging
2
+ from abc import abstractmethod
3
+ from typing import Optional
4
+
5
+ from lightautoml.automl.presets.tabular_presets import TabularAutoML
6
+ from lightautoml.tasks import Task
7
+ from pyspark.sql import DataFrame
8
+
9
+ from replay.utils.spark_utils import convert2spark, get_top_k_recs
10
+
11
+
12
+ class ReRanker:
13
+ """
14
+ Base class for models which re-rank recommendations produced by other models.
15
+ May be used as a part of two-stages recommendation pipeline.
16
+ """
17
+
18
+ _logger: Optional[logging.Logger] = None
19
+
20
+ @property
21
+ def logger(self) -> logging.Logger:
22
+ """
23
+ :returns: get library logger
24
+ """
25
+ if self._logger is None:
26
+ self._logger = logging.getLogger("replay")
27
+ return self._logger
28
+
29
+ @abstractmethod
30
+ def fit(self, data: DataFrame, fit_params: Optional[dict] = None) -> None:
31
+ """
32
+ Fit the model which re-rank user-item pairs generated outside the models.
33
+
34
+ :param data: spark dataframe with obligatory ``[user_idx, item_idx, target]``
35
+ columns and features' columns
36
+ :param fit_params: dict of parameters to pass to model.fit()
37
+ """
38
+
39
+ @abstractmethod
40
+ def predict(self, data, k) -> DataFrame:
41
+ """
42
+ Re-rank data with the model and get top-k recommendations for each user.
43
+
44
+ :param data: spark dataframe with obligatory ``[user_idx, item_idx]``
45
+ columns and features' columns
46
+ :param k: number of recommendations for each user
47
+ """
48
+
49
+
50
+ class LamaWrap(ReRanker):
51
+ """
52
+ LightAutoML TabularPipeline binary classification model wrapper for recommendations re-ranking.
53
+ Read more: https://github.com/sberbank-ai-lab/LightAutoML
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ params: Optional[dict] = None,
59
+ config_path: Optional[str] = None,
60
+ ):
61
+ """
62
+ Initialize LightAutoML TabularPipeline with passed params/configuration file.
63
+
64
+ :param params: dict of model parameters
65
+ :param config_path: path to configuration file
66
+ """
67
+ self.model = TabularAutoML(
68
+ task=Task("binary"),
69
+ config_path=config_path,
70
+ **(params if params is not None else {}),
71
+ )
72
+
73
+ def fit(self, data: DataFrame, fit_params: Optional[dict] = None) -> None:
74
+ """
75
+ Fit the LightAutoML TabularPipeline model with binary classification task.
76
+ Data should include negative and positive user-item pairs.
77
+
78
+ :param data: spark dataframe with obligatory ``[user_idx, item_idx, target]``
79
+ columns and features' columns. `Target` column should consist of zeros and ones
80
+ as the model is a binary classification model.
81
+ :param fit_params: dict of parameters to pass to model.fit()
82
+ See LightAutoML TabularPipeline fit_predict parameters.
83
+ """
84
+
85
+ params = {"roles": {"target": "target"}, "verbose": 1}
86
+ params.update({} if fit_params is None else fit_params)
87
+ data = data.drop("user_idx", "item_idx")
88
+ data_pd = data.toPandas()
89
+ self.model.fit_predict(data_pd, **params)
90
+
91
+ def predict(self, data: DataFrame, k: int) -> DataFrame:
92
+ """
93
+ Re-rank data with the model and get top-k recommendations for each user.
94
+
95
+ :param data: spark dataframe with obligatory ``[user_idx, item_idx]``
96
+ columns and features' columns
97
+ :param k: number of recommendations for each user
98
+ :return: spark dataframe with top-k recommendations for each user
99
+ the dataframe columns are ``[user_idx, item_idx, relevance]``
100
+ """
101
+ data_pd = data.toPandas()
102
+ candidates_ids = data_pd[["user_idx", "item_idx"]]
103
+ data_pd.drop(columns=["user_idx", "item_idx"], inplace=True)
104
+ self.logger.info("Starting re-ranking")
105
+ candidates_pred = self.model.predict(data_pd)
106
+ candidates_ids.loc[:, "relevance"] = candidates_pred.data[:, 0]
107
+ self.logger.info(
108
+ "%s candidates rated for %s users",
109
+ candidates_ids.shape[0],
110
+ candidates_ids["user_idx"].nunique(),
111
+ )
112
+
113
+ self.logger.info("top-k")
114
+ return get_top_k_recs(
115
+ recs=convert2spark(candidates_ids),
116
+ k=k,
117
+ )