replay-rec 0.18.0__py3-none-any.whl → 0.18.0rc0__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 (58) 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 +602 -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 +10 -0
  18. replay/experimental/models/admm_slim.py +205 -0
  19. replay/experimental/models/base_neighbour_rec.py +204 -0
  20. replay/experimental/models/base_rec.py +1271 -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 +923 -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 +265 -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/implicit_wrap.py +131 -0
  32. replay/experimental/models/lightfm_wrap.py +302 -0
  33. replay/experimental/models/mult_vae.py +332 -0
  34. replay/experimental/models/neuromf.py +406 -0
  35. replay/experimental/models/scala_als.py +296 -0
  36. replay/experimental/nn/data/__init__.py +1 -0
  37. replay/experimental/nn/data/schema_builder.py +55 -0
  38. replay/experimental/preprocessing/__init__.py +3 -0
  39. replay/experimental/preprocessing/data_preparator.py +839 -0
  40. replay/experimental/preprocessing/padder.py +229 -0
  41. replay/experimental/preprocessing/sequence_generator.py +208 -0
  42. replay/experimental/scenarios/__init__.py +1 -0
  43. replay/experimental/scenarios/obp_wrapper/__init__.py +8 -0
  44. replay/experimental/scenarios/obp_wrapper/obp_optuna_objective.py +74 -0
  45. replay/experimental/scenarios/obp_wrapper/replay_offline.py +248 -0
  46. replay/experimental/scenarios/obp_wrapper/utils.py +87 -0
  47. replay/experimental/scenarios/two_stages/__init__.py +0 -0
  48. replay/experimental/scenarios/two_stages/reranker.py +117 -0
  49. replay/experimental/scenarios/two_stages/two_stages_scenario.py +757 -0
  50. replay/experimental/utils/__init__.py +0 -0
  51. replay/experimental/utils/logger.py +24 -0
  52. replay/experimental/utils/model_handler.py +186 -0
  53. replay/experimental/utils/session_handler.py +44 -0
  54. {replay_rec-0.18.0.dist-info → replay_rec-0.18.0rc0.dist-info}/METADATA +11 -3
  55. replay_rec-0.18.0rc0.dist-info/NOTICE +41 -0
  56. {replay_rec-0.18.0.dist-info → replay_rec-0.18.0rc0.dist-info}/RECORD +58 -5
  57. {replay_rec-0.18.0.dist-info → replay_rec-0.18.0rc0.dist-info}/WHEEL +1 -1
  58. {replay_rec-0.18.0.dist-info → replay_rec-0.18.0rc0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,248 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from typing import (
4
+ Any,
5
+ Dict,
6
+ List,
7
+ Optional,
8
+ )
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ from obp.policy.base import BaseOfflinePolicyLearner
13
+ from optuna import create_study
14
+ from optuna.samplers import TPESampler
15
+ from pyspark.sql import DataFrame
16
+
17
+ from replay.data import Dataset, FeatureHint, FeatureInfo, FeatureSchema, FeatureType
18
+ from replay.experimental.scenarios.obp_wrapper.obp_optuna_objective import OBPObjective
19
+ from replay.experimental.scenarios.obp_wrapper.utils import split_bandit_feedback
20
+ from replay.models.base_rec import BaseRecommender
21
+ from replay.utils.spark_utils import convert2spark
22
+
23
+
24
+ def obp2df(action: np.ndarray, reward: np.ndarray, timestamp: np.ndarray) -> Optional[pd.DataFrame]:
25
+ """
26
+ Converts OBP log to the pandas DataFrame
27
+ """
28
+
29
+ n_interactions = len(action)
30
+
31
+ df = pd.DataFrame(
32
+ {
33
+ "user_idx": np.arange(n_interactions),
34
+ "item_idx": action,
35
+ "rating": reward,
36
+ "timestamp": timestamp,
37
+ }
38
+ )
39
+
40
+ return df
41
+
42
+
43
+ def context2df(context: np.ndarray, idx_col: np.ndarray, idx_col_name: str) -> Optional[pd.DataFrame]:
44
+ """
45
+ Converts OBP log to the pandas DataFrame
46
+ """
47
+
48
+ df1 = pd.DataFrame({idx_col_name + "_idx": idx_col})
49
+ cols = [str(i) + "_" + idx_col_name for i in range(context.shape[1])]
50
+ df2 = pd.DataFrame(context, columns=cols)
51
+
52
+ return df1.join(df2)
53
+
54
+
55
+ @dataclass
56
+ class OBPOfflinePolicyLearner(BaseOfflinePolicyLearner):
57
+ """
58
+ Off-policy learner which wraps OBP data representation into replay format.
59
+
60
+ :param n_actions: Number of actions.
61
+
62
+ :param len_list: Length of a list of actions in a recommendation/ranking inferface,
63
+ slate size. When Open Bandit Dataset is used, 3 should be set.
64
+
65
+ :param replay_model: Any model from replay library with fit, predict functions.
66
+
67
+ :param dataset: Dataset of interactions (user_id, item_id, rating).
68
+ Constructing inside the fit method. Used for predict of replay_model.
69
+ """
70
+
71
+ replay_model: Optional[BaseRecommender] = None
72
+ log: Optional[DataFrame] = None
73
+ max_usr_id: int = 0
74
+ item_features: DataFrame = None
75
+ _study = None
76
+ _logger: Optional[logging.Logger] = None
77
+ _objective = OBPObjective
78
+
79
+ def __post_init__(self) -> None:
80
+ """Initialize Class."""
81
+ self.feature_schema = FeatureSchema(
82
+ [
83
+ FeatureInfo(
84
+ column="user_idx",
85
+ feature_type=FeatureType.CATEGORICAL,
86
+ feature_hint=FeatureHint.QUERY_ID,
87
+ ),
88
+ FeatureInfo(
89
+ column="item_idx",
90
+ feature_type=FeatureType.CATEGORICAL,
91
+ feature_hint=FeatureHint.ITEM_ID,
92
+ ),
93
+ FeatureInfo(
94
+ column="rating",
95
+ feature_type=FeatureType.NUMERICAL,
96
+ feature_hint=FeatureHint.RATING,
97
+ ),
98
+ FeatureInfo(
99
+ column="timestamp",
100
+ feature_type=FeatureType.NUMERICAL,
101
+ feature_hint=FeatureHint.TIMESTAMP,
102
+ ),
103
+ ]
104
+ )
105
+
106
+ @property
107
+ def logger(self) -> logging.Logger:
108
+ """
109
+ :return: get library logger
110
+ """
111
+ if self._logger is None:
112
+ self._logger = logging.getLogger("replay")
113
+ return self._logger
114
+
115
+ def fit(
116
+ self,
117
+ action: np.ndarray,
118
+ reward: np.ndarray,
119
+ timestamp: np.ndarray,
120
+ context: np.ndarray = None,
121
+ action_context: np.ndarray = None,
122
+ ) -> None:
123
+ """
124
+ Fits an offline bandit policy on the given logged bandit data.
125
+ This `fit` method wraps bandit data and calls `fit` method for the replay_model.
126
+
127
+ :param action: Actions sampled by the logging/behavior policy
128
+ for each data in logged bandit data, i.e., :math:`a_i`.
129
+
130
+ :param reward: Rewards observed for each data in logged bandit data, i.e., :math:`r_i`.
131
+
132
+ :param timestamp: Moment of time when user interacted with corresponding item.
133
+
134
+ :param context: Context vectors observed for each data, i.e., :math:`x_i`.
135
+
136
+ :param action_context: Context vectors observed for each action.
137
+ """
138
+
139
+ log = convert2spark(obp2df(action, reward, timestamp))
140
+ self.log = log
141
+
142
+ user_features = None
143
+ self.max_usr_id = reward.shape[0]
144
+
145
+ if context is not None:
146
+ user_features = convert2spark(context2df(context, np.arange(context.shape[0]), "user"))
147
+
148
+ if action_context is not None:
149
+ self.item_features = convert2spark(context2df(action_context, np.arange(self.n_actions), "item"))
150
+
151
+ dataset = Dataset(
152
+ feature_schema=self.feature_schema,
153
+ interactions=log,
154
+ query_features=user_features,
155
+ item_features=self.item_features,
156
+ )
157
+ self.replay_model._fit_wrap(dataset)
158
+
159
+ def predict(self, n_rounds: int = 1, context: np.ndarray = None) -> np.ndarray:
160
+ """Predict best actions for new data.
161
+ Action set predicted by this `predict` method can contain duplicate items.
162
+ If a non-repetitive action set is needed, please use the `sample_action` method.
163
+
164
+ :context: Context vectors for new data.
165
+
166
+ :return: Action choices made by a classifier, which can contain duplicate items.
167
+ If a non-repetitive action set is needed, please use the `sample_action` method.
168
+ """
169
+
170
+ user_features = None
171
+ if context is not None:
172
+ user_features = convert2spark(
173
+ context2df(context, np.arange(self.max_usr_id, self.max_usr_id + n_rounds), "user")
174
+ )
175
+
176
+ users = convert2spark(pd.DataFrame({"user_idx": np.arange(self.max_usr_id, self.max_usr_id + n_rounds)}))
177
+ items = convert2spark(pd.DataFrame({"item_idx": np.arange(self.n_actions)}))
178
+
179
+ self.max_usr_id += n_rounds
180
+
181
+ dataset = Dataset(
182
+ feature_schema=self.feature_schema,
183
+ interactions=self.log,
184
+ query_features=user_features,
185
+ item_features=self.item_features,
186
+ check_consistency=False,
187
+ )
188
+
189
+ action_dist = self.replay_model._predict_proba(dataset, self.len_list, users, items, filter_seen_items=False)
190
+
191
+ return action_dist
192
+
193
+ def optimize(
194
+ self,
195
+ bandit_feedback: Dict[str, np.ndarray],
196
+ val_size: float = 0.3,
197
+ param_borders: Optional[Dict[str, List[Any]]] = None,
198
+ criterion: str = "ipw",
199
+ budget: int = 10,
200
+ new_study: bool = True,
201
+ ) -> Optional[Dict[str, Any]]:
202
+ """Optimize model parameters using optuna.
203
+ Optimization is carried out over the IPW/DR/DM scores(IPW by default).
204
+
205
+ :param bandit_feedback: Bandit log data with fields
206
+ ``[action, reward, context, action_context,
207
+ n_rounds, n_actions, position, pscore]`` as in OpenBanditPipeline.
208
+
209
+ :param val_size: Size of validation subset.
210
+
211
+ :param param_borders: Dictionary of parameter names with pair of borders
212
+ for the parameters optimization algorithm.
213
+
214
+ :param criterion: Score for optimization. Available are `ipw`, `dr` and `dm`.
215
+
216
+ :param budget: Number of trials for the optimization algorithm.
217
+
218
+ :param new_study: Flag to create new study or not for optuna.
219
+
220
+ :return: Dictionary of parameter names with optimal value of corresponding parameter.
221
+ """
222
+
223
+ bandit_feedback_train, bandit_feedback_val = split_bandit_feedback(bandit_feedback, val_size)
224
+
225
+ if self.replay_model._search_space is None:
226
+ self.logger.warning("%s has no hyper parameters to optimize", str(self))
227
+ return None
228
+
229
+ if self._study is None or new_study:
230
+ self._study = create_study(direction="maximize", sampler=TPESampler())
231
+
232
+ search_space = self.replay_model._prepare_param_borders(param_borders)
233
+ if self.replay_model._init_params_in_search_space(search_space) and not self.replay_model._params_tried():
234
+ self._study.enqueue_trial(self.replay_model._init_args)
235
+
236
+ objective = self._objective(
237
+ search_space=search_space,
238
+ bandit_feedback_train=bandit_feedback_train,
239
+ bandit_feedback_val=bandit_feedback_val,
240
+ learner=self,
241
+ criterion=criterion,
242
+ k=self.len_list,
243
+ )
244
+
245
+ self._study.optimize(objective, budget)
246
+ best_params = self._study.best_params
247
+ self.replay_model.set_params(**best_params)
248
+ return best_params
@@ -0,0 +1,87 @@
1
+ from typing import Dict, List, Tuple
2
+
3
+ import numpy as np
4
+ from obp.ope import RegressionModel
5
+ from sklearn.linear_model import LogisticRegression
6
+
7
+
8
+ def get_est_rewards_by_reg(n_actions, len_list, bandit_feedback_train, bandit_feedback_test):
9
+ """
10
+ Fit Logistic Regression to rewards from `bandit_feedback`.
11
+ """
12
+ regression_model = RegressionModel(
13
+ n_actions=n_actions,
14
+ len_list=len_list,
15
+ action_context=bandit_feedback_train["action_context"],
16
+ base_model=LogisticRegression(max_iter=1000, random_state=12345),
17
+ )
18
+
19
+ regression_model.fit(
20
+ context=bandit_feedback_train["context"],
21
+ action=bandit_feedback_train["action"],
22
+ reward=bandit_feedback_train["reward"],
23
+ position=bandit_feedback_train["position"],
24
+ pscore=bandit_feedback_train["pscore"],
25
+ )
26
+
27
+ estimated_rewards_by_reg_model = regression_model.predict(
28
+ context=bandit_feedback_test["context"],
29
+ )
30
+
31
+ return estimated_rewards_by_reg_model
32
+
33
+
34
+ def bandit_subset(borders: List[int], bandit_feedback: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
35
+ """
36
+ This function returns subset of a `bandit_feedback`
37
+ with borders specified in `borders`.
38
+
39
+ :param bandit_feedback: Bandit log data with fields
40
+ ``[action, reward, context, action_context,
41
+ n_rounds, n_actions, position, pscore]``
42
+ as in OpenBanditPipeline.
43
+ :param borders: List with two values ``[left, right]``
44
+ :return: Returns subset of a `bandit_feedback` for each key with
45
+ indexes from `left`(including) to `right`(excluding).
46
+ """
47
+ assert len(borders) == 2
48
+
49
+ left, right = borders
50
+
51
+ assert left < right
52
+
53
+ position = None if bandit_feedback["position"] is None else bandit_feedback["position"][left:right]
54
+
55
+ return {
56
+ "n_rounds": right - left,
57
+ "n_actions": bandit_feedback["n_actions"],
58
+ "action": bandit_feedback["action"][left:right],
59
+ "position": position,
60
+ "reward": bandit_feedback["reward"][left:right],
61
+ "pscore": bandit_feedback["pscore"][left:right],
62
+ "context": bandit_feedback["context"][left:right],
63
+ "action_context": bandit_feedback["action_context"][left:right],
64
+ }
65
+
66
+
67
+ def split_bandit_feedback(
68
+ bandit_feedback: Dict[str, np.ndarray], val_size: int = 0.3
69
+ ) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray]]:
70
+ """
71
+ Split `bandit_feedback` into two subsets.
72
+ :param bandit_feedback: Bandit log data with fields
73
+ ``[action, reward, context, action_context,
74
+ n_rounds, n_actions, position, pscore]``
75
+ as in OpenBanditPipeline.
76
+ :param val_size: Number in range ``[0, 1]`` corresponding to the proportion of
77
+ train/val split.
78
+ :return: `bandit_feedback_train` and `bandit_feedback_val` split.
79
+ """
80
+
81
+ n_rounds = bandit_feedback["n_rounds"]
82
+ n_rounds_train = int(n_rounds * (1.0 - val_size))
83
+
84
+ bandit_feedback_train = bandit_subset([0, n_rounds_train], bandit_feedback)
85
+ bandit_feedback_val = bandit_subset([n_rounds_train, n_rounds], bandit_feedback)
86
+
87
+ 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 Dict, 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
+ )