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.
Files changed (31) hide show
  1. corec-0.1.0/LICENSE +0 -0
  2. corec-0.1.0/PKG-INFO +17 -0
  3. corec-0.1.0/README.md +0 -0
  4. corec-0.1.0/corec/__init__.py +0 -0
  5. corec-0.1.0/corec/elliot_predict/___init__.py +0 -0
  6. corec-0.1.0/corec/elliot_predict/elliot_predict.py +106 -0
  7. corec-0.1.0/corec/evaluation/__init__.py +0 -0
  8. corec-0.1.0/corec/evaluation/constants.py +72 -0
  9. corec-0.1.0/corec/evaluation/evaluator.py +416 -0
  10. corec-0.1.0/corec/evaluation/metric_generator.py +324 -0
  11. corec-0.1.0/corec/evaluation/qrels_generator.py +88 -0
  12. corec-0.1.0/corec/evaluation/run_generator.py +293 -0
  13. corec-0.1.0/corec/evaluation/utils.py +41 -0
  14. corec-0.1.0/corec/postfilters/postfilter.py +132 -0
  15. corec-0.1.0/corec/recommenders/__init__.py +0 -0
  16. corec-0.1.0/corec/recommenders/base_recommender.py +67 -0
  17. corec-0.1.0/corec/recommenders/heuristic_recommender.py +171 -0
  18. corec-0.1.0/corec/recommenders/heuristic_recommenders/__init__.py +11 -0
  19. corec-0.1.0/corec/recommenders/heuristic_recommenders/context_pop.py +19 -0
  20. corec-0.1.0/corec/recommenders/heuristic_recommenders/context_random.py +17 -0
  21. corec-0.1.0/corec/recommenders/heuristic_recommenders/context_satisfaction.py +47 -0
  22. corec-0.1.0/corec/recommenders/heuristic_recommenders/context_similarity.py +163 -0
  23. corec-0.1.0/corec/recommenders/recbole_recommender.py +140 -0
  24. corec-0.1.0/corec/utils/recommenders_utils.py +110 -0
  25. corec-0.1.0/corec.egg-info/PKG-INFO +17 -0
  26. corec-0.1.0/corec.egg-info/SOURCES.txt +29 -0
  27. corec-0.1.0/corec.egg-info/dependency_links.txt +1 -0
  28. corec-0.1.0/corec.egg-info/requires.txt +6 -0
  29. corec-0.1.0/corec.egg-info/top_level.txt +1 -0
  30. corec-0.1.0/pyproject.toml +25 -0
  31. 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
+ )