corec 0.1.0__tar.gz
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.
- corec-0.1.0/LICENSE +0 -0
- corec-0.1.0/PKG-INFO +17 -0
- corec-0.1.0/README.md +0 -0
- corec-0.1.0/corec/__init__.py +0 -0
- corec-0.1.0/corec/elliot_predict/___init__.py +0 -0
- corec-0.1.0/corec/elliot_predict/elliot_predict.py +106 -0
- corec-0.1.0/corec/evaluation/__init__.py +0 -0
- corec-0.1.0/corec/evaluation/constants.py +72 -0
- corec-0.1.0/corec/evaluation/evaluator.py +416 -0
- corec-0.1.0/corec/evaluation/metric_generator.py +324 -0
- corec-0.1.0/corec/evaluation/qrels_generator.py +88 -0
- corec-0.1.0/corec/evaluation/run_generator.py +293 -0
- corec-0.1.0/corec/evaluation/utils.py +41 -0
- corec-0.1.0/corec/postfilters/postfilter.py +132 -0
- corec-0.1.0/corec/recommenders/__init__.py +0 -0
- corec-0.1.0/corec/recommenders/base_recommender.py +67 -0
- corec-0.1.0/corec/recommenders/heuristic_recommender.py +171 -0
- corec-0.1.0/corec/recommenders/heuristic_recommenders/__init__.py +11 -0
- corec-0.1.0/corec/recommenders/heuristic_recommenders/context_pop.py +19 -0
- corec-0.1.0/corec/recommenders/heuristic_recommenders/context_random.py +17 -0
- corec-0.1.0/corec/recommenders/heuristic_recommenders/context_satisfaction.py +47 -0
- corec-0.1.0/corec/recommenders/heuristic_recommenders/context_similarity.py +163 -0
- corec-0.1.0/corec/recommenders/recbole_recommender.py +140 -0
- corec-0.1.0/corec/utils/recommenders_utils.py +110 -0
- corec-0.1.0/corec.egg-info/PKG-INFO +17 -0
- corec-0.1.0/corec.egg-info/SOURCES.txt +29 -0
- corec-0.1.0/corec.egg-info/dependency_links.txt +1 -0
- corec-0.1.0/corec.egg-info/requires.txt +6 -0
- corec-0.1.0/corec.egg-info/top_level.txt +1 -0
- corec-0.1.0/pyproject.toml +25 -0
- corec-0.1.0/setup.cfg +4 -0
corec-0.1.0/LICENSE
ADDED
|
File without changes
|
corec-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: corec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Context-aware recommender systems framework
|
|
5
|
+
Author-email: Jaime Gimillo <jaimegimillo@gmail.com>
|
|
6
|
+
License: LICENSE
|
|
7
|
+
Keywords: recommender,context-aware,evaluation,framework
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: ==3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pydantic
|
|
15
|
+
Provides-Extra: recommender
|
|
16
|
+
Provides-Extra: evaluator
|
|
17
|
+
Requires-Dist: ranx; extra == "evaluator"
|
corec-0.1.0/README.md
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from elliot.run import run_experiment
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def prepare_temp_file(file_path: str):
|
|
15
|
+
"""
|
|
16
|
+
Creates a temporary file containing the first three columns of the input file,
|
|
17
|
+
formatted as a tab-separated values (TSV) file without a header.
|
|
18
|
+
"""
|
|
19
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".tsv") as temp_file:
|
|
20
|
+
with open(file_path, "r") as f_in:
|
|
21
|
+
df = pd.read_csv(f_in, sep="\t")
|
|
22
|
+
|
|
23
|
+
df = df.iloc[:, :3]
|
|
24
|
+
df.to_csv(temp_file.name, sep="\t", index=False, header=False)
|
|
25
|
+
|
|
26
|
+
return temp_file.name
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def unify_elliot_predictions_files(
|
|
30
|
+
folder_path: str, dest_folder_path: str, dest_filename: str, feat_names: list = None
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Organizes prediction files from a folder by model, selects the latest file for each model,
|
|
34
|
+
adds a header, compresses it into gzip format, and saves it to a specified destination folder.
|
|
35
|
+
Older files are removed.
|
|
36
|
+
"""
|
|
37
|
+
folder_path = Path(folder_path).resolve()
|
|
38
|
+
dest_folder_path = Path(dest_folder_path).resolve()
|
|
39
|
+
files = [file for file in folder_path.iterdir() if file.is_file()]
|
|
40
|
+
|
|
41
|
+
model_files = {}
|
|
42
|
+
pattern = r"^(?P<model>\w+)_"
|
|
43
|
+
|
|
44
|
+
for file in files:
|
|
45
|
+
match = re.match(pattern, file.name)
|
|
46
|
+
model = match.group("model") if match else file.name.rsplit(".", 1)[0]
|
|
47
|
+
|
|
48
|
+
if model not in model_files:
|
|
49
|
+
model_files[model] = []
|
|
50
|
+
model_files[model].append(file)
|
|
51
|
+
|
|
52
|
+
for model, files in model_files.items():
|
|
53
|
+
files.sort(key=lambda f: f.stat().st_ctime, reverse=True)
|
|
54
|
+
last_file = files[0]
|
|
55
|
+
|
|
56
|
+
model_folder = dest_folder_path / model
|
|
57
|
+
model_folder.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
new_file_path = model_folder / dest_filename.format(model=model)
|
|
59
|
+
|
|
60
|
+
with open(last_file, "rt+") as f_in:
|
|
61
|
+
content = f_in.read()
|
|
62
|
+
f_in.seek(0, 0)
|
|
63
|
+
f_in.write("\t".join(feat_names) + "\n" + content)
|
|
64
|
+
|
|
65
|
+
with open(last_file, "rb") as f_in:
|
|
66
|
+
with gzip.open(new_file_path, "wb") as f_out:
|
|
67
|
+
shutil.copyfileobj(f_in, f_out)
|
|
68
|
+
|
|
69
|
+
for file in files:
|
|
70
|
+
file.unlink()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def elliot_predict(
|
|
74
|
+
train_file_path: str,
|
|
75
|
+
test_file_path: str,
|
|
76
|
+
config_file_path: str,
|
|
77
|
+
valid_file_path: str = None,
|
|
78
|
+
):
|
|
79
|
+
"""
|
|
80
|
+
Prepares temporary training, testing and optionally validation files, updates the configuration
|
|
81
|
+
file for an 'elliot' experiment, runs the experiment, and cleans up the temporary files. Returns
|
|
82
|
+
the path to the folder with prediction results.
|
|
83
|
+
"""
|
|
84
|
+
temp_train_file_path = prepare_temp_file(train_file_path)
|
|
85
|
+
temp_test_file_path = prepare_temp_file(test_file_path)
|
|
86
|
+
|
|
87
|
+
with open(config_file_path, "r") as config_file:
|
|
88
|
+
config = yaml.safe_load(config_file)
|
|
89
|
+
|
|
90
|
+
config["experiment"]["data_config"]["train_path"] = temp_train_file_path
|
|
91
|
+
config["experiment"]["data_config"]["test_path"] = temp_test_file_path
|
|
92
|
+
predictions_folder_path = config["experiment"]["path_output_rec_result"]
|
|
93
|
+
|
|
94
|
+
if valid_file_path is not None:
|
|
95
|
+
temp_valid_file_path = prepare_temp_file(valid_file_path)
|
|
96
|
+
config["experiment"]["data_config"]["validation_path"] = temp_valid_file_path
|
|
97
|
+
|
|
98
|
+
with open(config_file_path, "w") as config_file:
|
|
99
|
+
yaml.safe_dump(config, config_file)
|
|
100
|
+
|
|
101
|
+
run_experiment(config_file_path)
|
|
102
|
+
|
|
103
|
+
os.remove(temp_train_file_path)
|
|
104
|
+
os.remove(temp_test_file_path)
|
|
105
|
+
|
|
106
|
+
return predictions_folder_path
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# ------- #
|
|
2
|
+
# METRICS #
|
|
3
|
+
# ------- #
|
|
4
|
+
|
|
5
|
+
CUTOFF_RANX_METRICS = [
|
|
6
|
+
"hits",
|
|
7
|
+
"hit_rate",
|
|
8
|
+
"precision",
|
|
9
|
+
"recall",
|
|
10
|
+
"f1",
|
|
11
|
+
"mrr",
|
|
12
|
+
"map",
|
|
13
|
+
"dcg",
|
|
14
|
+
"dcg_burges",
|
|
15
|
+
"ndcg",
|
|
16
|
+
"ndcg_burges",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
NON_CUTOFF_RANX_METRICS = [
|
|
20
|
+
"r_precision",
|
|
21
|
+
"bpref",
|
|
22
|
+
"rbp",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
RANX_METRICS = CUTOFF_RANX_METRICS + NON_CUTOFF_RANX_METRICS
|
|
26
|
+
|
|
27
|
+
CUSTOM_METRICS = [
|
|
28
|
+
"mean_ctx_sat",
|
|
29
|
+
"acc_ctx_sat",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# ---- #
|
|
33
|
+
# FUSE #
|
|
34
|
+
# ---- #
|
|
35
|
+
|
|
36
|
+
RANX_FUSE_METHODS = [
|
|
37
|
+
"bayesfuse",
|
|
38
|
+
"bordafuse",
|
|
39
|
+
"anz",
|
|
40
|
+
"gmnz",
|
|
41
|
+
"max",
|
|
42
|
+
"med",
|
|
43
|
+
"min",
|
|
44
|
+
"mnz",
|
|
45
|
+
"sum",
|
|
46
|
+
"condorcet",
|
|
47
|
+
"isr",
|
|
48
|
+
"log_isr",
|
|
49
|
+
"logn_isr",
|
|
50
|
+
"mapfuse",
|
|
51
|
+
"mixed",
|
|
52
|
+
"posfuse",
|
|
53
|
+
"probfuse",
|
|
54
|
+
"rbc",
|
|
55
|
+
"rrf",
|
|
56
|
+
"segfuse",
|
|
57
|
+
"slidefuse",
|
|
58
|
+
"w_bordafuse",
|
|
59
|
+
"w_condorcet",
|
|
60
|
+
"wmnz",
|
|
61
|
+
"wsum",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
RANX_FUSE_NORMS = [
|
|
65
|
+
"min-max",
|
|
66
|
+
"min-max-inverted",
|
|
67
|
+
"max",
|
|
68
|
+
"sum",
|
|
69
|
+
"zmuv",
|
|
70
|
+
"rank",
|
|
71
|
+
"borda",
|
|
72
|
+
]
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from pydantic import (
|
|
5
|
+
BaseModel,
|
|
6
|
+
Field,
|
|
7
|
+
FilePath,
|
|
8
|
+
NonNegativeInt,
|
|
9
|
+
PositiveInt,
|
|
10
|
+
PrivateAttr,
|
|
11
|
+
validate_arguments,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .metric_generator import MetricGenerator, FuseRun
|
|
15
|
+
from .qrels_generator import QrelsGenerator
|
|
16
|
+
from .run_generator import RunGenerator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Evaluator(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Class that integrates the functionality of QrelsGenerator, RunGenerator, and
|
|
22
|
+
MetricGenerator for streamlined evaluation.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
test_path: str = Field(
|
|
26
|
+
...,
|
|
27
|
+
description="Path to the test data.",
|
|
28
|
+
)
|
|
29
|
+
preds_path_template: str = Field(
|
|
30
|
+
...,
|
|
31
|
+
description="Template for the prediction file path. All placeholders '{model}' will be dynamically replaced with the model name.",
|
|
32
|
+
examples=["predictions/{model}/{model}_preds.txt"],
|
|
33
|
+
)
|
|
34
|
+
train_path: Optional[FilePath] = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
description="Path to the training data.",
|
|
37
|
+
)
|
|
38
|
+
valid_path: Optional[FilePath] = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
description="Path to the validation data.",
|
|
41
|
+
)
|
|
42
|
+
dataset_user_idx: NonNegativeInt = Field(
|
|
43
|
+
default=0,
|
|
44
|
+
description="Index for the user id column in the dataset.",
|
|
45
|
+
)
|
|
46
|
+
dataset_item_idx: NonNegativeInt = Field(
|
|
47
|
+
default=1,
|
|
48
|
+
description="Index for the item id column in the dataset.",
|
|
49
|
+
)
|
|
50
|
+
dataset_rating_idx: NonNegativeInt = Field(
|
|
51
|
+
default=2,
|
|
52
|
+
description="Index for the rating column in the dataset.",
|
|
53
|
+
)
|
|
54
|
+
dataset_ctx_idxs: Optional[List[NonNegativeInt]] = Field(
|
|
55
|
+
default=None,
|
|
56
|
+
description="Context column indexes in the dataset. If None, all columns except user, item, and rating are used.",
|
|
57
|
+
)
|
|
58
|
+
dataset_sep: str = Field(
|
|
59
|
+
default="\t",
|
|
60
|
+
description="Separator used in the dataset files.",
|
|
61
|
+
)
|
|
62
|
+
dataset_compression: Optional[str] = Field(
|
|
63
|
+
default=None,
|
|
64
|
+
description="Compression type used in the dataset files.",
|
|
65
|
+
)
|
|
66
|
+
preds_user_idx: NonNegativeInt = Field(
|
|
67
|
+
default=0,
|
|
68
|
+
description="Index for the user id column in the predictions.",
|
|
69
|
+
)
|
|
70
|
+
preds_item_idx: NonNegativeInt = Field(
|
|
71
|
+
default=1,
|
|
72
|
+
description="Index for the item id column in the predictions.",
|
|
73
|
+
)
|
|
74
|
+
preds_score_idx: NonNegativeInt = Field(
|
|
75
|
+
default=2,
|
|
76
|
+
description="Index for the score column in the predictions.",
|
|
77
|
+
)
|
|
78
|
+
preds_test_item_idx: NonNegativeInt = Field(
|
|
79
|
+
default=3,
|
|
80
|
+
description="Index for the test item id column in the predictions.",
|
|
81
|
+
)
|
|
82
|
+
preds_sep: str = Field(
|
|
83
|
+
default="\t",
|
|
84
|
+
description="Separator used in the predictions file.",
|
|
85
|
+
)
|
|
86
|
+
preds_compression: Optional[str] = Field(
|
|
87
|
+
default=None,
|
|
88
|
+
description="Compression type used in the predictions file.",
|
|
89
|
+
)
|
|
90
|
+
runs_path_template: Optional[str] = Field(
|
|
91
|
+
default=None,
|
|
92
|
+
description=(
|
|
93
|
+
"Template for the Runs output path. All placeholders '{run}' will be dynamically replaced with the run name. "
|
|
94
|
+
"If not specified, runs dictionaries will not be saved."
|
|
95
|
+
),
|
|
96
|
+
examples=["evaluation/{run}_run.json"],
|
|
97
|
+
)
|
|
98
|
+
rating_thr: NonNegativeInt = Field(
|
|
99
|
+
int=0,
|
|
100
|
+
description="Rating threshold for determining relevance in Qrels.",
|
|
101
|
+
)
|
|
102
|
+
num_processors: PositiveInt = Field(
|
|
103
|
+
default=1,
|
|
104
|
+
description="Number of processes to run in parallel.",
|
|
105
|
+
)
|
|
106
|
+
_qrels_gen = PrivateAttr(default=None)
|
|
107
|
+
_run_gen = PrivateAttr()
|
|
108
|
+
_metric_gen = PrivateAttr(default=None)
|
|
109
|
+
_qrels = PrivateAttr(default_factory=dict)
|
|
110
|
+
_contextual_runs = PrivateAttr(default_factory=dict)
|
|
111
|
+
_pure_non_ctx_runs = PrivateAttr(default_factory=dict)
|
|
112
|
+
_postfilter_runs = PrivateAttr(default_factory=dict)
|
|
113
|
+
_fuse_runs = PrivateAttr(default_factory=dict)
|
|
114
|
+
|
|
115
|
+
class Config:
|
|
116
|
+
extra = "forbid"
|
|
117
|
+
|
|
118
|
+
def model_post_init(self, _):
|
|
119
|
+
self._qrels_gen = QrelsGenerator(
|
|
120
|
+
test_path=self.test_path,
|
|
121
|
+
user_idx=self.dataset_user_idx,
|
|
122
|
+
item_idx=self.dataset_item_idx,
|
|
123
|
+
rating_idx=self.dataset_rating_idx,
|
|
124
|
+
data_sep=self.dataset_sep,
|
|
125
|
+
data_compression=self.dataset_compression,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if self.dataset_ctx_idxs is None:
|
|
129
|
+
dataset_ncols = pd.read_csv(
|
|
130
|
+
self.test_path,
|
|
131
|
+
sep=self.dataset_sep,
|
|
132
|
+
compression=self.dataset_compression,
|
|
133
|
+
).shape[1]
|
|
134
|
+
|
|
135
|
+
excluded_idxs = {
|
|
136
|
+
self.dataset_user_idx,
|
|
137
|
+
self.dataset_item_idx,
|
|
138
|
+
self.dataset_rating_idx,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
self.dataset_ctx_idxs = [
|
|
142
|
+
idx for idx in range(dataset_ncols) if idx not in excluded_idxs
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
self._run_gen = RunGenerator(
|
|
146
|
+
test_path=self.test_path,
|
|
147
|
+
train_path=self.train_path,
|
|
148
|
+
valid_path=self.valid_path,
|
|
149
|
+
context_idxs=self.dataset_ctx_idxs,
|
|
150
|
+
dataset_sep=self.dataset_sep,
|
|
151
|
+
dataset_compression=self.dataset_compression,
|
|
152
|
+
preds_user_idx=self.preds_user_idx,
|
|
153
|
+
preds_item_idx=self.preds_item_idx,
|
|
154
|
+
preds_score_idx=self.preds_score_idx,
|
|
155
|
+
preds_test_item_idx=self.preds_test_item_idx,
|
|
156
|
+
preds_sep=self.preds_sep,
|
|
157
|
+
preds_compression=self.preds_compression,
|
|
158
|
+
num_processors=self.num_processors,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _get_preds_path(self, model_name: str):
|
|
162
|
+
return self.preds_path_template.replace("{model}", model_name)
|
|
163
|
+
|
|
164
|
+
def _get_run_output_path(self, run_name: str):
|
|
165
|
+
if self.runs_path_template is None:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
return self.runs_path_template.replace("{run}", run_name)
|
|
169
|
+
|
|
170
|
+
@validate_arguments
|
|
171
|
+
def get_computed_summary(self):
|
|
172
|
+
"""
|
|
173
|
+
Returns a dictionary summarizing the progress of computations. The dictionary includes:
|
|
174
|
+
|
|
175
|
+
- Whether Qrels have been computed.
|
|
176
|
+
- The names of Runs generated for contextual, pure non-contextual, and post-filtered models.
|
|
177
|
+
- The names of fused Runs computed from the above models.
|
|
178
|
+
"""
|
|
179
|
+
return {
|
|
180
|
+
"qrels": len(self._qrels) != 0,
|
|
181
|
+
"runs": {
|
|
182
|
+
"contextual": list(self._contextual_runs.keys()),
|
|
183
|
+
"non-contextual": {
|
|
184
|
+
"pure": list(self._pure_non_ctx_runs.keys()),
|
|
185
|
+
"postfilter": list(self._postfilter_runs.keys()),
|
|
186
|
+
},
|
|
187
|
+
"fuse": [
|
|
188
|
+
{
|
|
189
|
+
"runs": fuse_run.fused_run_names,
|
|
190
|
+
"norm": fuse_run.norm,
|
|
191
|
+
"method": fuse_run.method,
|
|
192
|
+
}
|
|
193
|
+
for fuse_run in self._fuse_runs.values()
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@validate_arguments
|
|
199
|
+
def compute_qrels(self, output_path: Optional[str] = None):
|
|
200
|
+
"""
|
|
201
|
+
Computes the test data Qrels using the class rating threshold and
|
|
202
|
+
optionally saves the Qrels dictionary in a JSON file.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
`output_path`: `output_path`: Path to save the Qrels dictionary as a JSON file. If not specified, the Qrels is not saved.
|
|
206
|
+
"""
|
|
207
|
+
self._qrels = self._qrels_gen.compute_qrels(
|
|
208
|
+
rating_thr=self.rating_thr,
|
|
209
|
+
output_path=output_path,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
self._metric_gen = MetricGenerator(
|
|
213
|
+
qrels=self._qrels,
|
|
214
|
+
train_path=self.train_path,
|
|
215
|
+
valid_path=self.valid_path,
|
|
216
|
+
dataset_item_idx=self.dataset_item_idx,
|
|
217
|
+
dataset_ctx_idxs=self.dataset_ctx_idxs,
|
|
218
|
+
dataset_sep=self.dataset_sep,
|
|
219
|
+
dataset_compression=self.dataset_compression,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
@validate_arguments
|
|
223
|
+
def compute_contextual_run(self, ctx_model_name: str, run_name: str = None):
|
|
224
|
+
"""
|
|
225
|
+
Generates the Run for the specified context-aware model and, if `runs_path_template` was
|
|
226
|
+
specified during initialization, saves the Run dictionary in the corresponding JSON file.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
`ctx_model_name`: Name of context-aware model for which Run will be generated.
|
|
230
|
+
`run_name`: Name assigned to the generated Run and to be displayed in metrics tables. If None, the model name will be used as the default value.
|
|
231
|
+
"""
|
|
232
|
+
if run_name is None:
|
|
233
|
+
run_name = ctx_model_name
|
|
234
|
+
|
|
235
|
+
predictions_path = self._get_preds_path(ctx_model_name)
|
|
236
|
+
output_path = self._get_run_output_path(run_name)
|
|
237
|
+
|
|
238
|
+
run = self._run_gen.compute_contextual_run(
|
|
239
|
+
predictions_path=predictions_path,
|
|
240
|
+
output_path=output_path,
|
|
241
|
+
)
|
|
242
|
+
run.name = run_name
|
|
243
|
+
self._contextual_runs[run.name] = run
|
|
244
|
+
|
|
245
|
+
@validate_arguments
|
|
246
|
+
def compute_pure_non_contextual_run(
|
|
247
|
+
self,
|
|
248
|
+
non_ctx_model_name: str,
|
|
249
|
+
run_name: str = None,
|
|
250
|
+
K: int = None,
|
|
251
|
+
):
|
|
252
|
+
"""
|
|
253
|
+
Generates the Run for the specified non-context-aware model without post-filtering and, if
|
|
254
|
+
`runs_path_template` was specified during initialization, saves the Runs dictionaries
|
|
255
|
+
in the corresponding JSON file.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
`non_ctx_model_name`: Name of non-context-aware model for which Run will be generated.
|
|
259
|
+
`run_name`: Name assigned to the generated Run and to be displayed in metrics tables. If None, the model name will be used as the default value.
|
|
260
|
+
`K`: Number of top predictions to retain per user. By default all recommendations will be considered.
|
|
261
|
+
"""
|
|
262
|
+
if run_name is None:
|
|
263
|
+
run_name = non_ctx_model_name
|
|
264
|
+
|
|
265
|
+
predictions_path = self._get_preds_path(non_ctx_model_name)
|
|
266
|
+
output_path = self._get_run_output_path(run_name)
|
|
267
|
+
|
|
268
|
+
if run_name is None:
|
|
269
|
+
run_name = non_ctx_model_name
|
|
270
|
+
|
|
271
|
+
run = self._run_gen.compute_non_contextual_run(
|
|
272
|
+
predictions_path=predictions_path,
|
|
273
|
+
K=K,
|
|
274
|
+
output_path=output_path,
|
|
275
|
+
)
|
|
276
|
+
run.name = run_name
|
|
277
|
+
self._pure_non_ctx_runs[run.name] = run
|
|
278
|
+
|
|
279
|
+
@validate_arguments
|
|
280
|
+
def compute_postfilter_run(
|
|
281
|
+
self,
|
|
282
|
+
non_ctx_model_name: str,
|
|
283
|
+
run_name: str,
|
|
284
|
+
K: int = None,
|
|
285
|
+
):
|
|
286
|
+
"""
|
|
287
|
+
Generates Runs for the specified non-context-aware models with post-filtering applied
|
|
288
|
+
and, if `runs_path_template` was specified during initialization, saves the Runs dictionaries
|
|
289
|
+
in the corresponding JSON file.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
`non_ctx_models_names`: List of names of non-context-aware models for which Runs will be generated.
|
|
293
|
+
`run_name`: Name assigned to the generated Run and to be displayed in metrics tables. If None, the model name will be used as the default value.
|
|
294
|
+
`K`: Number of top predictions to retain per user. By default all recommendations will be considered.
|
|
295
|
+
"""
|
|
296
|
+
if run_name is None:
|
|
297
|
+
run_name = non_ctx_model_name
|
|
298
|
+
|
|
299
|
+
predictions_path = self._get_preds_path(non_ctx_model_name)
|
|
300
|
+
output_path = self._get_run_output_path(run_name)
|
|
301
|
+
|
|
302
|
+
run = self._run_gen.compute_non_contextual_run(
|
|
303
|
+
predictions_path=predictions_path,
|
|
304
|
+
context_postfilter=True,
|
|
305
|
+
K=K,
|
|
306
|
+
output_path=output_path,
|
|
307
|
+
)
|
|
308
|
+
run.name = run_name
|
|
309
|
+
self._postfilter_runs[run.name] = run
|
|
310
|
+
|
|
311
|
+
@validate_arguments
|
|
312
|
+
def compute_fuse_run(
|
|
313
|
+
self,
|
|
314
|
+
ctx_run_names: List[str] = [],
|
|
315
|
+
pure_non_ctx_run_names: List[str] = [],
|
|
316
|
+
postfilter_run_names: List[str] = [],
|
|
317
|
+
norm: str = "min-max",
|
|
318
|
+
method: str = "wsum",
|
|
319
|
+
run_name: str = None,
|
|
320
|
+
):
|
|
321
|
+
"""
|
|
322
|
+
Generates a fused Run by combining the specified computed Runs, and normalization and
|
|
323
|
+
fusion methods. The computed Run is optionally saved to a JSON file.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
`ctx_run_names`: List of names of computed contextual Runs to be fused.
|
|
327
|
+
`pure_non_ctx_run_names`: List of names of computed pure non-contextual Runs to be fused.
|
|
328
|
+
`postfilter_run_names`: List of names of computed post-filtered Runs to be fused.
|
|
329
|
+
`norm`: Ranx normalization method to apply before fusion.
|
|
330
|
+
`method`: Ranx fusion method to apply.
|
|
331
|
+
`run_name`: Name assigned to the generated Run. If None, a concatenation between the model names, norm and method will be used as the default value.
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
`RuntimeError`: If any specified Run was not previously computed.
|
|
335
|
+
"""
|
|
336
|
+
runs_to_check = {
|
|
337
|
+
"contextual": (ctx_run_names, self._contextual_runs),
|
|
338
|
+
"pure non-contextual": (pure_non_ctx_run_names, self._pure_non_ctx_runs),
|
|
339
|
+
"postfilter": (postfilter_run_names, self._postfilter_runs),
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
runs_to_fuse = []
|
|
343
|
+
for run_type, (check_names, run_dict) in runs_to_check.items():
|
|
344
|
+
for check_name in check_names:
|
|
345
|
+
if check_name not in run_dict:
|
|
346
|
+
raise RuntimeError(
|
|
347
|
+
f"{check_name} Run of type '{run_type}' is not yet computed."
|
|
348
|
+
)
|
|
349
|
+
runs_to_fuse.append(run_dict[check_name])
|
|
350
|
+
|
|
351
|
+
if not len(runs_to_fuse):
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
fused_run_names = [run.name for run in runs_to_fuse]
|
|
355
|
+
if run_name is None:
|
|
356
|
+
run_name = "+".join(fused_run_names)
|
|
357
|
+
run_name += f"_{norm}_{method}"
|
|
358
|
+
|
|
359
|
+
output_path = self._get_run_output_path(run_name)
|
|
360
|
+
|
|
361
|
+
run = self._run_gen.compute_fuse_run(
|
|
362
|
+
runs=runs_to_fuse, norm=norm, method=method, output_path=output_path
|
|
363
|
+
)
|
|
364
|
+
run.name = run_name
|
|
365
|
+
|
|
366
|
+
self._fuse_runs[run.name] = FuseRun(
|
|
367
|
+
run=run,
|
|
368
|
+
fused_run_names=fused_run_names,
|
|
369
|
+
norm=norm,
|
|
370
|
+
method=method,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
@validate_arguments
|
|
374
|
+
def compute_metrics(
|
|
375
|
+
self,
|
|
376
|
+
non_fuse_output_path: str,
|
|
377
|
+
fuse_output_path: str,
|
|
378
|
+
metrics: List[str] = [],
|
|
379
|
+
cutoffs: List[PositiveInt] = [],
|
|
380
|
+
):
|
|
381
|
+
"""
|
|
382
|
+
Compute and save metrics in the specified CSV files for all the computed Runs.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
`non_fuse_output_path`: Path to the CSV file where metrics for non-fused runs will be saved.
|
|
386
|
+
`fuse_output_path`: Path to the CSV file where metrics for fused runs will be saved.
|
|
387
|
+
`metrics`: List of metrics to compute.
|
|
388
|
+
`cutoffs`: List of cutoffs to consider when calculating the metrics.
|
|
389
|
+
|
|
390
|
+
Raises:
|
|
391
|
+
`RuntimeError`: If Qrels were not previously computed.
|
|
392
|
+
"""
|
|
393
|
+
if self._metric_gen is None:
|
|
394
|
+
raise RuntimeError("Qrels haven't beeen computer yet.")
|
|
395
|
+
|
|
396
|
+
non_fuse_runs = (
|
|
397
|
+
list(self._contextual_runs.values())
|
|
398
|
+
+ list(self._pure_non_ctx_runs.values())
|
|
399
|
+
+ list(self._postfilter_runs.values())
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
self._metric_gen.compute_non_fuse_runs_metrics(
|
|
403
|
+
output_path=non_fuse_output_path,
|
|
404
|
+
runs=non_fuse_runs,
|
|
405
|
+
metrics=metrics,
|
|
406
|
+
cutoffs=cutoffs,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
fuse_runs = list(self._fuse_runs.values())
|
|
410
|
+
|
|
411
|
+
self._metric_gen.compute_fuse_runs_metrics(
|
|
412
|
+
output_path=fuse_output_path,
|
|
413
|
+
fuse_runs=fuse_runs,
|
|
414
|
+
metrics=metrics,
|
|
415
|
+
cutoffs=cutoffs,
|
|
416
|
+
)
|