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.
- FranKGraphBench-0.0.1a0.dist-info/LICENSE +201 -0
- FranKGraphBench-0.0.1a0.dist-info/METADATA +189 -0
- FranKGraphBench-0.0.1a0.dist-info/RECORD +70 -0
- FranKGraphBench-0.0.1a0.dist-info/WHEEL +6 -0
- FranKGraphBench-0.0.1a0.dist-info/entry_points.txt +3 -0
- FranKGraphBench-0.0.1a0.dist-info/top_level.txt +2 -0
- data_integration/__init__.py +47 -0
- data_integration/__main__.py +4 -0
- data_integration/dataset.py +270 -0
- data_integration/dataset2class.py +34 -0
- data_integration/datasets/__init__.py +0 -0
- data_integration/datasets/amazon.py +140 -0
- data_integration/datasets/book-crossing.py +98 -0
- data_integration/datasets/douban-movie.py +75 -0
- data_integration/datasets/lastfm.py +215 -0
- data_integration/datasets/movielens.py +375 -0
- data_integration/datasets/steam.py +98 -0
- data_integration/datasets/worker.py +31 -0
- data_integration/datasets/yelp.py +156 -0
- framework/__init__.py +17 -0
- framework/__main__.py +4 -0
- framework/dataloader/__init__.py +0 -0
- framework/dataloader/dataloader.py +52 -0
- framework/dataloader/dataset/__init__.py +0 -0
- framework/dataloader/dataset/dataset.py +38 -0
- framework/dataloader/edge_splitter/__init__.py +0 -0
- framework/dataloader/edge_splitter/edge_splitter.py +163 -0
- framework/dataloader/graph/__init__.py +0 -0
- framework/dataloader/graph/graph.py +314 -0
- framework/dataloader/graph/node.py +80 -0
- framework/dataloader/preprocess/__init__.py +0 -0
- framework/dataloader/preprocess/methods.py +47 -0
- framework/evaluator/__init__.py +0 -0
- framework/evaluator/metric.py +39 -0
- framework/evaluator/metric2class.py +10 -0
- framework/evaluator/metrics/__init__.py +0 -0
- framework/evaluator/metrics/map.py +33 -0
- framework/evaluator/metrics/ndcg.py +49 -0
- framework/experiment.py +120 -0
- framework/recommender/__init__.py +0 -0
- framework/recommender/model2class.py +46 -0
- framework/recommender/models/__init__.py +0 -0
- framework/recommender/models/complEx/__init__.py +0 -0
- framework/recommender/models/complEx/model.py +193 -0
- framework/recommender/models/deep_walk_based/__init__.py +0 -0
- framework/recommender/models/deep_walk_based/model.py +149 -0
- framework/recommender/models/distMult/__init__.py +0 -0
- framework/recommender/models/distMult/model.py +187 -0
- framework/recommender/models/ePHEN/__init__.py +0 -0
- framework/recommender/models/ePHEN/model.py +159 -0
- framework/recommender/models/rESCAL/__init__.py +0 -0
- framework/recommender/models/rESCAL/model.py +184 -0
- framework/recommender/models/rotatE/__init__.py +0 -0
- framework/recommender/models/rotatE/model.py +195 -0
- framework/recommender/models/transD/__init__.py +0 -0
- framework/recommender/models/transD/model.py +190 -0
- framework/recommender/models/transE/__init__.py +0 -0
- framework/recommender/models/transE/model.py +194 -0
- framework/recommender/models/transH/__init__.py +0 -0
- framework/recommender/models/transH/model.py +193 -0
- framework/recommender/models/transR/__init__.py +0 -0
- framework/recommender/models/transR/model.py +190 -0
- framework/recommender/models/tuckER/__init__.py +0 -0
- framework/recommender/models/tuckER/model.py +199 -0
- framework/recommender/recommender.py +32 -0
- framework/recommender/utils/__init__.py +0 -0
- framework/recommender/utils/walker.py +185 -0
- framework/reporter/__init__.py +0 -0
- framework/reporter/report.py +101 -0
- 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
|
+
|
|
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
|
+
|