FranKGraphBench 0.0.1a0__py2.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 (70) hide show
  1. FranKGraphBench-0.0.1a0.dist-info/LICENSE +201 -0
  2. FranKGraphBench-0.0.1a0.dist-info/METADATA +189 -0
  3. FranKGraphBench-0.0.1a0.dist-info/RECORD +70 -0
  4. FranKGraphBench-0.0.1a0.dist-info/WHEEL +6 -0
  5. FranKGraphBench-0.0.1a0.dist-info/entry_points.txt +3 -0
  6. FranKGraphBench-0.0.1a0.dist-info/top_level.txt +2 -0
  7. data_integration/__init__.py +47 -0
  8. data_integration/__main__.py +4 -0
  9. data_integration/dataset.py +270 -0
  10. data_integration/dataset2class.py +34 -0
  11. data_integration/datasets/__init__.py +0 -0
  12. data_integration/datasets/amazon.py +140 -0
  13. data_integration/datasets/book-crossing.py +98 -0
  14. data_integration/datasets/douban-movie.py +75 -0
  15. data_integration/datasets/lastfm.py +215 -0
  16. data_integration/datasets/movielens.py +375 -0
  17. data_integration/datasets/steam.py +98 -0
  18. data_integration/datasets/worker.py +31 -0
  19. data_integration/datasets/yelp.py +156 -0
  20. framework/__init__.py +17 -0
  21. framework/__main__.py +4 -0
  22. framework/dataloader/__init__.py +0 -0
  23. framework/dataloader/dataloader.py +52 -0
  24. framework/dataloader/dataset/__init__.py +0 -0
  25. framework/dataloader/dataset/dataset.py +38 -0
  26. framework/dataloader/edge_splitter/__init__.py +0 -0
  27. framework/dataloader/edge_splitter/edge_splitter.py +163 -0
  28. framework/dataloader/graph/__init__.py +0 -0
  29. framework/dataloader/graph/graph.py +314 -0
  30. framework/dataloader/graph/node.py +80 -0
  31. framework/dataloader/preprocess/__init__.py +0 -0
  32. framework/dataloader/preprocess/methods.py +47 -0
  33. framework/evaluator/__init__.py +0 -0
  34. framework/evaluator/metric.py +39 -0
  35. framework/evaluator/metric2class.py +10 -0
  36. framework/evaluator/metrics/__init__.py +0 -0
  37. framework/evaluator/metrics/map.py +33 -0
  38. framework/evaluator/metrics/ndcg.py +49 -0
  39. framework/experiment.py +120 -0
  40. framework/recommender/__init__.py +0 -0
  41. framework/recommender/model2class.py +46 -0
  42. framework/recommender/models/__init__.py +0 -0
  43. framework/recommender/models/complEx/__init__.py +0 -0
  44. framework/recommender/models/complEx/model.py +193 -0
  45. framework/recommender/models/deep_walk_based/__init__.py +0 -0
  46. framework/recommender/models/deep_walk_based/model.py +149 -0
  47. framework/recommender/models/distMult/__init__.py +0 -0
  48. framework/recommender/models/distMult/model.py +187 -0
  49. framework/recommender/models/ePHEN/__init__.py +0 -0
  50. framework/recommender/models/ePHEN/model.py +159 -0
  51. framework/recommender/models/rESCAL/__init__.py +0 -0
  52. framework/recommender/models/rESCAL/model.py +184 -0
  53. framework/recommender/models/rotatE/__init__.py +0 -0
  54. framework/recommender/models/rotatE/model.py +195 -0
  55. framework/recommender/models/transD/__init__.py +0 -0
  56. framework/recommender/models/transD/model.py +190 -0
  57. framework/recommender/models/transE/__init__.py +0 -0
  58. framework/recommender/models/transE/model.py +194 -0
  59. framework/recommender/models/transH/__init__.py +0 -0
  60. framework/recommender/models/transH/model.py +193 -0
  61. framework/recommender/models/transR/__init__.py +0 -0
  62. framework/recommender/models/transR/model.py +190 -0
  63. framework/recommender/models/tuckER/__init__.py +0 -0
  64. framework/recommender/models/tuckER/model.py +199 -0
  65. framework/recommender/recommender.py +32 -0
  66. framework/recommender/utils/__init__.py +0 -0
  67. framework/recommender/utils/walker.py +185 -0
  68. framework/reporter/__init__.py +0 -0
  69. framework/reporter/report.py +101 -0
  70. framework/utils.py +10 -0
@@ -0,0 +1,314 @@
1
+ from collections import defaultdict
2
+
3
+ from .node import *
4
+ from ...utils import get_optional_argument
5
+
6
+ import networkx as nx
7
+ import pandas as pd
8
+ from tqdm import tqdm
9
+ import numpy as np
10
+
11
+ from typing import Set, Tuple, Dict
12
+
13
+ """
14
+ Class wrapper on the top of a nx.Graph
15
+ """
16
+
17
+
18
+ class Graph(nx.Graph):
19
+
20
+ def __init__(self):
21
+ super().__init__()
22
+ self.datatype_separator = "::"
23
+ self.item_nodes = set()
24
+ self.user_nodes = set()
25
+ self.rating_edges = defaultdict(set) # { user: Set([items]) }
26
+ self.rating_item2users = defaultdict(set) # { item: Set([users]) }
27
+
28
+ def build(self, name, item, user, ratings, enrich=None, social=None):
29
+ # Adding item, user and properties nodes
30
+ self.name = name
31
+ self._add_item_info(item, enrich)
32
+ self._add_user_info(user)
33
+ self._add_ratings(ratings)
34
+ if social is not None:
35
+ self._add_social_links(social)
36
+
37
+ def info(self):
38
+ n_nodes = self.number_of_nodes()
39
+ n_edges = self.number_of_edges()
40
+ message = (
41
+ f"{self.name} graph with a total of {n_nodes} nodes and {n_edges} edges"
42
+ )
43
+ message += f" ({len(self.user_nodes)} users, {len(self.item_nodes)} items and {len(list(self.get_rating_edges()))} ratings)"
44
+ return message
45
+
46
+ def get_item_nodes(self) -> Set[ItemNode]:
47
+ return self.item_nodes
48
+
49
+ def get_user_nodes(self) -> Set[UserNode]:
50
+ return self.user_nodes
51
+
52
+ def get_rating_edges(self) -> Tuple[UserNode, ItemNode]:
53
+ for user, items in self.rating_edges.items():
54
+ for item in items:
55
+ yield (user, item)
56
+
57
+ def get_user_property_edges(self) -> Tuple[UserNode, PropertyNode]:
58
+ for node, edge in self.edges():
59
+ if isinstance(node, UserNode) and isinstance(edge, PropertyNode):
60
+ yield (node, edge)
61
+ elif isinstance(node, PropertyNode) and isinstance(edge, UserNode):
62
+ yield (edge, node)
63
+
64
+ def get_item_property_edges(self) -> Tuple[ItemNode, PropertyNode]:
65
+ for node, edge in self.edges():
66
+ if isinstance(node, ItemNode) and isinstance(edge, PropertyNode):
67
+ yield (node, edge)
68
+ elif isinstance(node, PropertyNode) and isinstance(edge, ItemNode):
69
+ yield (edge, node)
70
+
71
+ def get_ratings_with_labels(self):
72
+ ratings = defaultdict(list)
73
+ for u, v in list(self.get_rating_edges()):
74
+ data = self.get_edge_data(u, v)
75
+ ratings[u].append((v, data["rating"]))
76
+
77
+ return ratings
78
+
79
+ def get_all_triples(self):
80
+ triples_return = {"head": [], "relation": [], "tail": []}
81
+
82
+ # ratings triples
83
+ ratings = self.get_ratings_with_labels()
84
+ desc = f"Generating ratings triples"
85
+ n_total = len(ratings.keys())
86
+ for user, ratings in tqdm(ratings.items(), total=n_total, desc=desc):
87
+ ratings.sort(key=lambda x: x[1], reverse=True)
88
+ for rating in ratings:
89
+ triples_return["head"].append(user.__str__())
90
+ triples_return["relation"].append(f"rating{rating[1]}")
91
+ triples_return["tail"].append(rating[0].__str__())
92
+
93
+ # user property triples
94
+ user_properties = self.get_user_property_edges()
95
+ desc = f"Generating user properties triples"
96
+ for user, user_property in tqdm(user_properties, desc=desc):
97
+ triples_return["head"].append(user.__str__())
98
+ triples_return["relation"].append("is")
99
+ triples_return["tail"].append(user_property.__str__())
100
+
101
+ # item property triples
102
+ item_properties = self.get_item_property_edges()
103
+ desc = f"Generating item properties triples"
104
+ for item, item_property in tqdm(item_properties, desc=desc):
105
+ triples_return["head"].append(item.__str__())
106
+ triples_return["relation"].append("has")
107
+ triples_return["tail"].append(item_property.__str__())
108
+
109
+ return pd.DataFrame(triples_return)
110
+
111
+ def get_ratings_triples(self):
112
+ triples_return = {"head": [], "relation": [], "tail": []}
113
+
114
+ # ratings triples
115
+ ratings = self.get_ratings_with_labels()
116
+ desc = f"Generating ratings triples"
117
+ n_total = len(ratings.keys())
118
+ for user, ratings in tqdm(ratings.items(), total=n_total, desc=desc):
119
+ ratings.sort(key=lambda x: x[1], reverse=True)
120
+ for rating in ratings:
121
+ triples_return["head"].append(user.__str__())
122
+ triples_return["relation"].append(f"rating{rating[1]}")
123
+ triples_return["tail"].append(rating[0].__str__())
124
+
125
+ return pd.DataFrame(triples_return)
126
+
127
+ def get_user_rated_items(self, user: UserNode) -> Set[ItemNode]:
128
+ return self.rating_edges[user]
129
+
130
+ def add_node(self, node_for_adding: Node, **attr):
131
+ super().add_node(node_for_adding, **attr)
132
+ if isinstance(node_for_adding, ItemNode):
133
+ self.item_nodes.add(node_for_adding)
134
+ elif isinstance(node_for_adding, UserNode):
135
+ self.user_nodes.add(node_for_adding)
136
+
137
+ def add_edge(self, u_of_edge, v_of_edge, **attr):
138
+ super().add_edge(u_of_edge, v_of_edge, **attr)
139
+ data = self.get_edge_data(u_of_edge, v_of_edge, default={})
140
+ edge_type = data.get("type")
141
+
142
+ if edge_type == "rated":
143
+ self.rating_edges[u_of_edge].add(v_of_edge)
144
+ self.rating_item2users[v_of_edge].add(u_of_edge)
145
+
146
+ def remove_edge(self, u, v):
147
+ if isinstance(u, UserNode) and isinstance(v, ItemNode):
148
+ self.rating_edges[u].remove(v)
149
+ self.rating_item2users[v].remove(u)
150
+
151
+ super().remove_edge(u, v)
152
+
153
+ def remove_node(self, n):
154
+ super().remove_node(n)
155
+ if isinstance(n, ItemNode):
156
+ self.item_nodes.remove(n)
157
+ del self.rating_item2users[n]
158
+ for user, items in self.rating_edges.items():
159
+ if n in items:
160
+ self.rating_edges[user].remove(n)
161
+ elif isinstance(n, UserNode):
162
+ self.user_nodes.remove(n)
163
+ del self.rating_edges[n]
164
+ for item, users in self.rating_item2users.items():
165
+ if n in users:
166
+ self.rating_item2users[item].remove(n)
167
+
168
+ def convert_node_labels_to_integer(self):
169
+ N = self.number_of_nodes()
170
+ mapping = dict(zip(self.nodes(), range(0, N)))
171
+ G = nx.relabel_nodes(self, mapping, copy=True)
172
+ nx.set_node_attributes(G, {v: k for k, v in mapping.items()}, "old_label")
173
+
174
+ G.item_nodes = self.item_nodes.copy()
175
+ G.user_nodes = self.user_nodes.copy()
176
+ G.rating_edges = self.rating_edges.copy()
177
+ G.rating_item2users = self.rating_item2users.copy()
178
+
179
+ return G
180
+
181
+ def convert_back(self):
182
+ mapping = {node: self.nodes[node]["old_label"] for node in self.nodes()}
183
+ G = nx.relabel_nodes(self, mapping, copy=True)
184
+ nx.set_node_attributes(G, {v: k for k, v in mapping.items()}, "old_label")
185
+
186
+ G.item_nodes = self.item_nodes.copy()
187
+ G.user_nodes = self.user_nodes.copy()
188
+ G.rating_edges = self.rating_edges.copy()
189
+ G.rating_item2users = self.rating_item2users.copy()
190
+
191
+ return G
192
+
193
+ def __read_csv(self, path: str) -> pd.DataFrame:
194
+ df = pd.read_csv(path)
195
+ col_names = []
196
+ for col in df.columns:
197
+ try:
198
+ col_name, data_type = col.split("::")
199
+ col_names.append(col_name)
200
+
201
+ if data_type == "string":
202
+ df[col] = df[col].astype(str)
203
+ elif data_type == "string_list":
204
+ df[col] = df[col].apply(lambda x: str(x).split("::"))
205
+ elif data_type == "number":
206
+ df[col] = pd.to_numeric(df[col])
207
+ except Exception as e:
208
+ print(
209
+ f"Couldn't parse data type of column named {col}. Should be col_name::data_type"
210
+ )
211
+ print(f"Supported types are: string, string_list, number")
212
+ exit()
213
+ df.columns = col_names
214
+
215
+ return df
216
+
217
+ def _add_item_info(self, item, enrich):
218
+ # Extracting info from .csv
219
+ df_item = self.__read_csv(item["path"])
220
+ extra_features = get_optional_argument(item, "extra_features", [])
221
+ df_item = df_item[["item_id"] + extra_features]
222
+ if enrich is not None:
223
+ # Merging info from original dataset and enriched
224
+ df_map = self.__read_csv(enrich["map_path"])
225
+ df_item = pd.merge(df_item, df_map, on="item_id", how="left")
226
+ if enrich["remove_unmatched"]:
227
+ df_item = df_item[df_item["URI"].notna()]
228
+
229
+ df_enriched = self._get_enrich_dataframe(**enrich)
230
+ df_item = pd.merge(df_item, df_enriched, on="item_id", how="left")
231
+
232
+ # Creating graph from dataframe
233
+ desc = "Adding item info into network"
234
+ n_items = df_item.shape[0]
235
+ properties = list(df_item.columns)
236
+ properties.remove("item_id")
237
+
238
+ # Adding item nodes and properties nodes
239
+ for _, row in tqdm(df_item.iterrows(), total=n_items, desc=desc):
240
+ item_node = ItemNode(row["item_id"])
241
+ self.add_node(item_node)
242
+
243
+ for property_ in properties:
244
+ self._add_node_property(item_node, row, property_)
245
+
246
+ def _add_node_property(self, source: Node, row: pd.Series, property_: str):
247
+ prop_value = row[property_]
248
+ if not isinstance(prop_value, list) and pd.notna(prop_value):
249
+ # Single property_
250
+ property_node = PropertyNode(row[property_], property_)
251
+ self.add_edge(source, property_node, type="has_property")
252
+ elif isinstance(prop_value, list):
253
+ # Multiples properties
254
+ for value in row[property_]:
255
+ property_node = PropertyNode(value, property_)
256
+ self.add_edge(source, property_node, type="has_property")
257
+
258
+ def _get_optional_argument(self, config, key, default):
259
+ try:
260
+ return config[key]
261
+ except KeyError:
262
+ return default
263
+
264
+ def _get_enrich_dataframe(self, **enrich_config):
265
+ df_enriched = self.__read_csv(enrich_config["enrich_path"])
266
+ properties = enrich_config["properties"]
267
+ df_enriched = df_enriched[["item_id"] + properties]
268
+
269
+ return df_enriched
270
+
271
+ def _add_user_info(self, user):
272
+ df_user = self.__read_csv(user["path"])
273
+ extra_features = get_optional_argument(user, "extra_features", [])
274
+ df_user = df_user[["user_id"] + extra_features]
275
+
276
+ total_users = df_user.shape[0]
277
+ desc = "Adding user info into network"
278
+ properties = list(df_user.columns)
279
+ properties.remove("user_id")
280
+
281
+ for _, row in tqdm(df_user.iterrows(), total=total_users, desc=desc):
282
+ user_node = UserNode(row["user_id"])
283
+ self.add_node(user_node)
284
+
285
+ for property_ in properties:
286
+ self._add_node_property(user_node, row, property_)
287
+
288
+ def _add_ratings(self, ratings):
289
+ df_ratings = self.__read_csv(ratings["path"])
290
+ total_ratings = df_ratings.shape[0]
291
+ desc = "Adding ratings into network"
292
+
293
+ for _, row in tqdm(df_ratings.iterrows(), total=total_ratings, desc=desc):
294
+ item_node = ItemNode(row["item_id"])
295
+ user_node = UserNode(row["user_id"])
296
+
297
+ if item_node in self and user_node in self:
298
+ attrs = {"type": "rated", "rating": row["rating"]}
299
+ if ratings["timestamp"]:
300
+ attrs["timestamp"] = row["timestamp"]
301
+
302
+ self.add_edge(user_node, item_node, **attrs)
303
+
304
+ def _add_social_links(self, social):
305
+ df_social = self.__read_csv(social["path"])
306
+ total_links = df_social.shape[0]
307
+ desc = "Adding social links between users into network"
308
+
309
+ for _, row in tqdm(df_social.iterrows(), total=total_links, desc=desc):
310
+ user1_node = UserNode(row["user1"])
311
+ user2_node = UserNode(row["user2"])
312
+
313
+ if user1_node in self and user2_node in self:
314
+ self.add_edge(user1_node, user2_node)
@@ -0,0 +1,80 @@
1
+ """
2
+ Base node class
3
+ """
4
+ class Node():
5
+ def __init__(self, idx, **attrs):
6
+ self.id = idx
7
+ self.attrs = attrs
8
+
9
+ def __hash__(self):
10
+ return hash(str(self))
11
+
12
+ def get_id(self):
13
+ return self.id
14
+
15
+ """
16
+ Node of type Item class
17
+ """
18
+ class ItemNode(Node):
19
+ def __init__(self, idx, **attrs):
20
+ super().__init__(idx, **attrs)
21
+
22
+ def __str__(self):
23
+ return f'Item({self.id})'
24
+
25
+ def __repr__(self):
26
+ return str(self)
27
+
28
+ def __hash__(self):
29
+ return super().__hash__()
30
+
31
+ def __eq__(self, other):
32
+ if isinstance(self, ItemNode) and isinstance(other, ItemNode):
33
+ return self.id == other.id
34
+ return False
35
+
36
+ """
37
+ Node of type User class
38
+ """
39
+ class UserNode(Node):
40
+ def __init__(self, idx, **attrs):
41
+ super().__init__(idx, **attrs)
42
+
43
+ def __str__(self):
44
+ return f'User({self.id})'
45
+
46
+ def __repr__(self):
47
+ return str(self)
48
+
49
+ def __hash__(self):
50
+ return super().__hash__()
51
+
52
+ def __eq__(self, other):
53
+ if isinstance(self, UserNode) and isinstance(other, UserNode):
54
+ return self.id == other.id
55
+ return False
56
+
57
+ """
58
+ Node of type Property class
59
+ """
60
+ class PropertyNode(Node):
61
+ def __init__(self, idx, property_type, **attrs):
62
+ super().__init__(idx, **attrs)
63
+ self.property_type = property_type
64
+
65
+ def __str__(self):
66
+ return f'Property({self.id}, {self.property_type})'
67
+
68
+ def __repr__(self):
69
+ return str(self)
70
+
71
+ def __hash__(self):
72
+ return super().__hash__()
73
+
74
+ def __eq__(self, other):
75
+ if isinstance(self, PropertyNode) and isinstance(other, PropertyNode) and self.property_type == other.property_type:
76
+ return self.id == other.id
77
+ return False
78
+
79
+ def get_property_type(self):
80
+ return self.property_type
File without changes
@@ -0,0 +1,47 @@
1
+ from ..graph.graph import Graph
2
+
3
+ from tqdm import tqdm
4
+
5
+ def apply_method(G, **info):
6
+ method2fun = {
7
+ 'binarize': binarize,
8
+ 'filter_kcore': filter_kcore
9
+ }
10
+
11
+ try:
12
+ f = method2fun[info['method']]
13
+ f(G, **info['parameters'])
14
+ except KeyError:
15
+ raise KeyError(f'Preprocessing method {info["method"]} is not implemented.')
16
+
17
+ def binarize(G: Graph, threshold: int):
18
+ desc='Binarizing user rating'
19
+ for (user, item) in tqdm(list(G.get_rating_edges()), desc=desc):
20
+ edge_data = G.get_edge_data(user,item)
21
+
22
+ bin_rate = 0 # negative
23
+ if edge_data['rating'] >= threshold:
24
+ bin_rate = 1
25
+
26
+ G[user][item]['rating'] = bin_rate
27
+
28
+ def filter_kcore(G: Graph, k: int, target: str, iterations: int = 1):
29
+ print(f'K-core filtering (target={target}), where core={k} and {iterations} it.')
30
+ for it in range(iterations):
31
+ desc = f'K-core filtering {it+1} iteration over edges from each {target}'
32
+
33
+ data = None
34
+ if target == 'user':
35
+ data = G.rating_edges.copy()
36
+ elif target == 'item':
37
+ data = G.rating_item2users.copy()
38
+
39
+ n_total = len(list(data.keys()))
40
+
41
+ count = 0
42
+ for (u, vs) in tqdm(data.items(), total=n_total, desc=desc):
43
+ if len(vs) < k:
44
+ G.remove_node(u)
45
+ count += 1
46
+ if count == 0:
47
+ break
File without changes
@@ -0,0 +1,39 @@
1
+ class Metric:
2
+ def __init__(self, k : int, relevance_threshold : int = 0):
3
+ self.k = k
4
+ self.relevance_threshold = relevance_threshold
5
+
6
+ def name(self) -> str:
7
+ raise NotImplementedError('Implement the name() method for your metric subclass')
8
+
9
+ def eval(self, ratings, recommendations):
10
+ """
11
+ Evaluate recommendation metric over user ratings
12
+ :param ratings: ratings used as ground truth
13
+ of type {user: [(item1, rating1), (item2, rating2), ...]}
14
+ :param recommendations: model k first recommendations
15
+ of type {user: [(item1, rating1), (item2, rating2), ...]}
16
+ """
17
+ raise NotImplementedError('Implement the eval() method for your metric subclass')
18
+
19
+ def _get_relevant_ratings(self, ratings, with_ratings=False):
20
+ relevant_ratings = {}
21
+ for u, u_ratings in ratings.items():
22
+ relevant_items = []
23
+ for (i, rating) in u_ratings:
24
+ if rating >= self.relevance_threshold:
25
+ if not with_ratings:
26
+ relevant_items.append(i)
27
+ else:
28
+ relevant_items.append((i, rating))
29
+ else:
30
+ # It's sorted, so I'm sure the next ones wont be greater than threshold
31
+ break
32
+
33
+ if len(relevant_items) > 0:
34
+ relevant_ratings[u] = relevant_items
35
+
36
+ return relevant_ratings
37
+
38
+
39
+
@@ -0,0 +1,10 @@
1
+ metric2class = {
2
+ 'MAP': {
3
+ 'submodule': 'metrics.map',
4
+ 'class': 'MAP'
5
+ },
6
+ 'nDCG': {
7
+ 'submodule': 'metrics.ndcg',
8
+ 'class': 'nDCG'
9
+ }
10
+ }
File without changes
@@ -0,0 +1,33 @@
1
+ from ..metric import Metric
2
+
3
+ import numpy as np
4
+
5
+ class MAP(Metric):
6
+ # reference: https://github.com/benhamner/Metrics/blob/master/Python/ml_metrics/average_precision.py
7
+ def init(self, k : int, relevance_threshold=0):
8
+ super().init(k, relevance_threshold)
9
+
10
+ def name(self):
11
+ return f'MAP@{self.k}'
12
+
13
+ def eval(self, ratings, recommendations):
14
+ relevant_items = self._get_relevant_ratings(ratings)
15
+ users_apk = [self._apk(set(rel_items), recommendations[u]) for u, rel_items in relevant_items.items()]
16
+ return np.mean(users_apk)
17
+
18
+ def _apk(self, rel_items, recommendations):
19
+ if len(recommendations) > self.k:
20
+ recommendations = recommendations[:self.k]
21
+
22
+ score = 0.0
23
+ num_hits = 0.0
24
+
25
+ if not rel_items:
26
+ return score
27
+
28
+ for i, rec in enumerate(recommendations):
29
+ if rec in rel_items and rec not in recommendations[:i]:
30
+ num_hits += 1.0
31
+ score += num_hits/(i+1.0)
32
+
33
+ return score/min(len(rel_items), self.k)
@@ -0,0 +1,49 @@
1
+ from ..metric import Metric
2
+ import numpy as np
3
+
4
+ class nDCG(Metric):
5
+ def __init__(self, k: int, relevance_threshold: int = 0):
6
+ super().__init__(k, relevance_threshold)
7
+
8
+ def name(self):
9
+ return f'nDCG@{self.k}'
10
+
11
+ def eval(self, ratings, recommendations):
12
+ relevant_items = self._get_relevant_ratings(ratings, with_ratings=True)
13
+ users_ndcg = []
14
+ for u, rel_items in relevant_items.items():
15
+ u_recs = self._get_user_rec_relevance(rel_items, recommendations[u])
16
+ ndcg = self._calculate_ndcg(rel_items, u_recs)
17
+ users_ndcg.append(ndcg)
18
+
19
+ return np.mean(users_ndcg)
20
+
21
+ def _calculate_dcg(self, recs):
22
+ recs = np.asfarray(recs)[:self.k]
23
+ if not recs.size:
24
+ return 0.0
25
+ else:
26
+ return np.sum(recs / np.log2(np.arange(2, recs.size+2)))
27
+
28
+ def _get_user_rec_relevance(self, relevant_items, recs):
29
+ rel = {} # {item: float(rating)}
30
+ for (item, rating) in relevant_items:
31
+ rel[item] = rating
32
+
33
+ recs_relevance = []
34
+ for item in recs:
35
+ item_rel = float(rel.get(item, 0.0))
36
+ recs_relevance.append(item_rel)
37
+
38
+ return recs_relevance
39
+
40
+
41
+ def _calculate_ndcg(self, relevant_items, recs):
42
+ # already ordered
43
+ relevant_items = [rating for _, rating in relevant_items[:self.k]]
44
+ idcg = self._calculate_dcg(relevant_items)
45
+ if not idcg:
46
+ return 0.0
47
+ else:
48
+ return self._calculate_dcg(recs) / idcg
49
+