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,31 @@
|
|
|
1
|
+
from threading import Thread
|
|
2
|
+
import queue
|
|
3
|
+
|
|
4
|
+
class Worker(Thread):
|
|
5
|
+
def __init__(self, request_queue, query_function, pbar=None):
|
|
6
|
+
"""
|
|
7
|
+
Parameters:
|
|
8
|
+
request_queue: queue containing requests to be consumed by workers
|
|
9
|
+
query_function: function to make the query to the endpoint
|
|
10
|
+
pbar: tqdm progressbar to be updated after each query
|
|
11
|
+
"""
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.queue = request_queue
|
|
14
|
+
self.local_results = []
|
|
15
|
+
self.query = query_function
|
|
16
|
+
self.pbar = pbar
|
|
17
|
+
|
|
18
|
+
def run(self):
|
|
19
|
+
while True:
|
|
20
|
+
try:
|
|
21
|
+
idx, query = self.queue.get(block=False)
|
|
22
|
+
response = self.query(query)
|
|
23
|
+
self.local_results.append((idx, response))
|
|
24
|
+
if self.pbar is not None:
|
|
25
|
+
self.pbar.update(n=1)
|
|
26
|
+
self.queue.task_done()
|
|
27
|
+
except queue.Empty:
|
|
28
|
+
break
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(f'Exception:')
|
|
31
|
+
print(e)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
from ..dataset import Dataset
|
|
7
|
+
|
|
8
|
+
class Yelp(Dataset):
|
|
9
|
+
def __init__(self, input_path, output_path, n_workers=1):
|
|
10
|
+
super().__init__(input_path, output_path, n_workers)
|
|
11
|
+
self.dataset_name = "Yelp"
|
|
12
|
+
|
|
13
|
+
self.categories_separator = ", "
|
|
14
|
+
self.friends_separator = ", "
|
|
15
|
+
self.elites_separator = ","
|
|
16
|
+
|
|
17
|
+
self.string_list_separator = "::"
|
|
18
|
+
|
|
19
|
+
self.item_fields = {
|
|
20
|
+
"business_id": "item_id::string",
|
|
21
|
+
"name": "name::string",
|
|
22
|
+
"address": "address::string",
|
|
23
|
+
"city": "city::string",
|
|
24
|
+
"state": "state::string",
|
|
25
|
+
"postal_code": "postal_code::string",
|
|
26
|
+
"latitude": "latitute::number",
|
|
27
|
+
"longitude": "longitude::number",
|
|
28
|
+
"stars": "stars::number",
|
|
29
|
+
"review_count": "review_count::number",
|
|
30
|
+
"is_open": "is_open::number",
|
|
31
|
+
"attributes": "attributes::string_list",
|
|
32
|
+
"categories": "categories::string_list",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
self.user_fields = {
|
|
36
|
+
"user_id": "user_id::string",
|
|
37
|
+
"name": "name::string",
|
|
38
|
+
"review_count": "review_count::number",
|
|
39
|
+
"yelping_since": "yelping_since::string",
|
|
40
|
+
"useful": "useful_count::number",
|
|
41
|
+
"funny": "funny_count::number",
|
|
42
|
+
"cool": "cool_count::number",
|
|
43
|
+
"elite": "elite_years::string_list",
|
|
44
|
+
"fans": "fans_count::number",
|
|
45
|
+
"average_stars": "average_stars::number",
|
|
46
|
+
"compliment_hot": "compliment_hot_count::number",
|
|
47
|
+
"compliment_more": "compliment_more_count::number",
|
|
48
|
+
"compliment_profile": "compliment_cute_count::number",
|
|
49
|
+
"compliment_list": "compliment_list_count::number",
|
|
50
|
+
"compliment_note": "compliment_note_count::number",
|
|
51
|
+
"compliment_plain": "compliment_plain_count::number",
|
|
52
|
+
"compliment_cool": "compliment_cool_count::number",
|
|
53
|
+
"compliment_funny": "compliment_funny_count::number",
|
|
54
|
+
"compliment_writer": "compliment_writer_count::number",
|
|
55
|
+
"compliment_photos": "compliment_photos_count::number"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
self.rating_fields = {
|
|
59
|
+
"user_id": "user_id::string",
|
|
60
|
+
"business_id": "item_id::string",
|
|
61
|
+
"stars": "rating::number",
|
|
62
|
+
"useful": "useful_count::number",
|
|
63
|
+
"funny": "funny_count::number",
|
|
64
|
+
"cool": "cool_count::number",
|
|
65
|
+
"text": "text::string",
|
|
66
|
+
"date": "date::string"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
self.social_fields = {"user_id": "user1::string", "friend_id": "user2::string"}
|
|
70
|
+
|
|
71
|
+
def load_item_data(self) -> pd.DataFrame():
|
|
72
|
+
filename = os.path.join(self.input_path, "yelp_academic_dataset_business.json")
|
|
73
|
+
|
|
74
|
+
print(filename)
|
|
75
|
+
file = open(filename, encoding="utf8")
|
|
76
|
+
data = []
|
|
77
|
+
for line in file:
|
|
78
|
+
data.append(json.loads(line))
|
|
79
|
+
df = pd.DataFrame(data)
|
|
80
|
+
file.close()
|
|
81
|
+
|
|
82
|
+
attributes = defaultdict(list)
|
|
83
|
+
for index, row in df['attributes'].dropna().items():
|
|
84
|
+
for key, value in row.items():
|
|
85
|
+
if value == 'True':
|
|
86
|
+
attributes[index].append(key)
|
|
87
|
+
df['attributes'] = pd.Series(attributes).apply(lambda x: self.string_list_separator.join(x))
|
|
88
|
+
|
|
89
|
+
categories = defaultdict(list)
|
|
90
|
+
for index, row in df['categories'].dropna().items():
|
|
91
|
+
for category in row.split(self.categories_separator):
|
|
92
|
+
categories[index].append(category)
|
|
93
|
+
df['categories'] = pd.Series(categories).apply(lambda x: self.string_list_separator.join(x))
|
|
94
|
+
|
|
95
|
+
df = df.rename(self.item_fields, axis=1)
|
|
96
|
+
df = df[self.item_fields.values()]
|
|
97
|
+
return df
|
|
98
|
+
|
|
99
|
+
def load_user_data(self) -> pd.DataFrame:
|
|
100
|
+
filename = os.path.join(self.input_path, "yelp_academic_dataset_user.json")
|
|
101
|
+
|
|
102
|
+
file = open(filename, encoding="utf8")
|
|
103
|
+
data = []
|
|
104
|
+
for line in file:
|
|
105
|
+
data.append(json.loads(line))
|
|
106
|
+
df = pd.DataFrame(data)
|
|
107
|
+
file.close()
|
|
108
|
+
|
|
109
|
+
elites = defaultdict(list)
|
|
110
|
+
for index, row in df['elite'].dropna().items():
|
|
111
|
+
for elite in row.split(self.elites_separator):
|
|
112
|
+
elites[index].append(elite)
|
|
113
|
+
df['elite'] = pd.Series(elites).apply(lambda x: self.string_list_separator.join(x))
|
|
114
|
+
|
|
115
|
+
df = df.rename(self.user_fields, axis=1)
|
|
116
|
+
df = df[self.user_fields.values()]
|
|
117
|
+
return df
|
|
118
|
+
|
|
119
|
+
def load_rating_data(self) -> pd.DataFrame:
|
|
120
|
+
filename = os.path.join(self.input_path, "yelp_academic_dataset_review.json")
|
|
121
|
+
|
|
122
|
+
file = open(filename, encoding="utf8")
|
|
123
|
+
data = []
|
|
124
|
+
for line in file:
|
|
125
|
+
data.append(json.loads(line))
|
|
126
|
+
df = pd.DataFrame(data)
|
|
127
|
+
file.close()
|
|
128
|
+
df = df.rename(self.rating_fields, axis=1)
|
|
129
|
+
df = df[self.rating_fields.values()]
|
|
130
|
+
return df
|
|
131
|
+
|
|
132
|
+
def load_social_data(self) -> pd.DataFrame:
|
|
133
|
+
filename = os.path.join(self.input_path, "yelp_academic_dataset_user.json")
|
|
134
|
+
|
|
135
|
+
file = open(filename, encoding="utf8")
|
|
136
|
+
data = []
|
|
137
|
+
for line in file:
|
|
138
|
+
data.append(json.loads(line))
|
|
139
|
+
user_df = pd.DataFrame(data)
|
|
140
|
+
file.close()
|
|
141
|
+
|
|
142
|
+
friends = defaultdict(list)
|
|
143
|
+
for index, row in user_df['friends'].dropna().items():
|
|
144
|
+
for friend in row.split(self.friends_separator):
|
|
145
|
+
friends[index].append(friend)
|
|
146
|
+
|
|
147
|
+
social = {'user_id': [], 'friend_id': []}
|
|
148
|
+
for index, friends_list in friends.items():
|
|
149
|
+
for friend in friends_list:
|
|
150
|
+
social["user_id"].append(user_df['user_id'].iloc[index])
|
|
151
|
+
social["friend_id"].append(friend)
|
|
152
|
+
|
|
153
|
+
df = pd.DataFrame(social)
|
|
154
|
+
df = df.rename(self.social_fields, axis=1)
|
|
155
|
+
df = df[self.social_fields.values()]
|
|
156
|
+
return df
|
framework/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from framework.experiment import run
|
|
3
|
+
|
|
4
|
+
__version__ = "0.0.1-alpha"
|
|
5
|
+
__author__ = "Paulo do Carmo and Alvaro Lopes"
|
|
6
|
+
|
|
7
|
+
def framework():
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
description="Script to run framework for reproducible experiment."
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
parser.add_argument('-c', '--config', type=str, required=True, help='.yml config filepath to setup experiment')
|
|
13
|
+
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
config_path = args.config
|
|
16
|
+
print(f'Running experiment...')
|
|
17
|
+
run(config_path)
|
framework/__main__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from ..utils import get_optional_argument
|
|
2
|
+
from .graph.graph import Graph
|
|
3
|
+
from .preprocess.methods import apply_method
|
|
4
|
+
from .edge_splitter.edge_splitter import EdgeSplitter
|
|
5
|
+
from .dataset.dataset import Dataset
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Loads, preprocess, filter and split dataset, then return a dataset
|
|
9
|
+
:arguments:
|
|
10
|
+
dataset: dict specifying the dataset configuration
|
|
11
|
+
:returns: Networkx graph (train, val, test)
|
|
12
|
+
"""
|
|
13
|
+
def load(**data_config):
|
|
14
|
+
# G = Graph(**data_config)
|
|
15
|
+
G = Graph()
|
|
16
|
+
G.build(**data_config)
|
|
17
|
+
return G
|
|
18
|
+
|
|
19
|
+
def preprocess(G, methods: list):
|
|
20
|
+
for method in methods:
|
|
21
|
+
apply_method(G, **method)
|
|
22
|
+
|
|
23
|
+
def split(G, **split_config):
|
|
24
|
+
seed = get_optional_argument(split_config, 'seed', 42)
|
|
25
|
+
edge_splitter_test = EdgeSplitter(G, seed=seed)
|
|
26
|
+
for (G_train, ratings_test) in edge_splitter_test.split(**split_config['test']):
|
|
27
|
+
dataset = Dataset()
|
|
28
|
+
dataset.set_test_data(ratings_test)
|
|
29
|
+
print(f'\tDisturbed graph info after splitting test data: {G_train.info()}')
|
|
30
|
+
print(f'\tNumber of test instances: {len_instances(ratings_test)}')
|
|
31
|
+
|
|
32
|
+
if get_optional_argument(split_config, 'validation', False):
|
|
33
|
+
if split_config['test']['method'] == 'k_fold' and split_config['validation']['method'] == 'k_fold':
|
|
34
|
+
raise ValueError("Validation split does not support k_fold method.")
|
|
35
|
+
|
|
36
|
+
edge_splitter_val = EdgeSplitter(G_train, seed=seed)
|
|
37
|
+
for G_train, ratings_val in edge_splitter_val.split(**split_config['validation']):
|
|
38
|
+
dataset.set_val_data(ratings_val)
|
|
39
|
+
print(f'\tGraph info after splitting validation data: {G_train.info()}')
|
|
40
|
+
print(f'\tNumber of validation instances: {len_instances(ratings_val)}')
|
|
41
|
+
|
|
42
|
+
ratings_train = G_train.get_ratings_with_labels()
|
|
43
|
+
dataset.set_train_data(G_train, ratings_train)
|
|
44
|
+
print(f'Number of training instances: {len_instances(ratings_train)}')
|
|
45
|
+
|
|
46
|
+
yield dataset
|
|
47
|
+
|
|
48
|
+
def len_instances(data):
|
|
49
|
+
return sum([len(items) for items in data.values()])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class Dataset():
|
|
2
|
+
def __init__(self):
|
|
3
|
+
self.G_train = None
|
|
4
|
+
self.ratings_train = None # {user: [(item1, rating), ... } Unordered items
|
|
5
|
+
self.ratings_test = None
|
|
6
|
+
self.has_val = False
|
|
7
|
+
self.ratings_val = None
|
|
8
|
+
|
|
9
|
+
def set_train_data(self, G, ratings):
|
|
10
|
+
self.G_train = G
|
|
11
|
+
self._sort_ratings(ratings)
|
|
12
|
+
self.ratings_train = ratings
|
|
13
|
+
|
|
14
|
+
def set_test_data(self, ratings):
|
|
15
|
+
self._sort_ratings(ratings)
|
|
16
|
+
self.ratings_test = ratings
|
|
17
|
+
|
|
18
|
+
def set_val_data(self, ratings):
|
|
19
|
+
self.has_val = True
|
|
20
|
+
self._sort_ratings(ratings)
|
|
21
|
+
self.ratings_val = ratings
|
|
22
|
+
|
|
23
|
+
def get_train_data(self):
|
|
24
|
+
return self.G_train, self.ratings_train
|
|
25
|
+
|
|
26
|
+
def get_test_data(self):
|
|
27
|
+
return self.ratings_test
|
|
28
|
+
|
|
29
|
+
def get_val_data(self):
|
|
30
|
+
return self.ratings_val
|
|
31
|
+
|
|
32
|
+
def has_val_data(self):
|
|
33
|
+
return self.has_val
|
|
34
|
+
|
|
35
|
+
def _sort_ratings(self, ratings):
|
|
36
|
+
# inplace sorting
|
|
37
|
+
for u in ratings.keys():
|
|
38
|
+
ratings[u].sort(reverse=True, key=lambda x: x[1])
|
|
File without changes
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from typing import Tuple, Dict, List
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import math
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
from ..graph.graph import Graph
|
|
9
|
+
from ..graph.node import UserNode, ItemNode
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from sklearn.model_selection import KFold
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EdgeSplitter():
|
|
16
|
+
def __init__(self, G: Graph, seed : int = 42):
|
|
17
|
+
self.G = deepcopy(G)
|
|
18
|
+
self.random_state = seed
|
|
19
|
+
random.seed(seed)
|
|
20
|
+
np.random.seed(seed)
|
|
21
|
+
|
|
22
|
+
self.supported_methods = [
|
|
23
|
+
'random_by_ratio',
|
|
24
|
+
'timestamp_by_ratio',
|
|
25
|
+
'fixed_timestamp',
|
|
26
|
+
'k_fold'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
def split(self, **config) -> Tuple[Graph, Dict[UserNode, List[Tuple[ItemNode, float]]]]:
|
|
30
|
+
method = config['method']
|
|
31
|
+
if method not in self.supported_methods:
|
|
32
|
+
raise ValueError(f'Invalid split method provided. So far, the supported ones are:\n{self.supported_methods}')
|
|
33
|
+
|
|
34
|
+
if method in ['random_by_ratio', 'timestamp_by_ratio']:
|
|
35
|
+
p = config['p']
|
|
36
|
+
level = config["level"]
|
|
37
|
+
if p <= 0 or p >= 1:
|
|
38
|
+
raise ValueError('The parameter p must be in the interval (0,1)')
|
|
39
|
+
|
|
40
|
+
if level not in ['user', 'global']:
|
|
41
|
+
raise ValueError('Invalid level parameter for splitting. Choose between user and global level.')
|
|
42
|
+
|
|
43
|
+
print(f'Splitting data using {method} method at {level} level, where p={p} ...')
|
|
44
|
+
|
|
45
|
+
if method == 'random_by_ratio':
|
|
46
|
+
test = self._random_by_ratio(p, level)
|
|
47
|
+
elif method == 'timestamp_by_ratio':
|
|
48
|
+
test = self._timestamp_by_ratio(p, level)
|
|
49
|
+
|
|
50
|
+
yield self._extract_dataset(test)
|
|
51
|
+
|
|
52
|
+
elif method == 'fixed_timestamp':
|
|
53
|
+
print(f'Splitting data using {method} method, where timestamp={config["timestamp"]} ...')
|
|
54
|
+
|
|
55
|
+
test = []
|
|
56
|
+
for (u,v) in self.G.get_rating_edges():
|
|
57
|
+
data = self.G.get_edge_data(u,v)
|
|
58
|
+
if data['timestamp'] > config['timestamp']:
|
|
59
|
+
test.append((u,v))
|
|
60
|
+
yield self._extract_dataset(test)
|
|
61
|
+
|
|
62
|
+
elif method == 'k_fold':
|
|
63
|
+
k = config['k']
|
|
64
|
+
if k < 2:
|
|
65
|
+
raise ValueError('k parameter for K-fold must be at least 2.')
|
|
66
|
+
|
|
67
|
+
level = config['level']
|
|
68
|
+
if level not in ['user', 'global']:
|
|
69
|
+
raise ValueError('Invalid level parameter for splitting. Choose between user and global level.')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
for test in self._kfold(k, level):
|
|
73
|
+
yield self._extract_dataset(test)
|
|
74
|
+
|
|
75
|
+
def _extract_dataset(self, test):
|
|
76
|
+
# Returns Graph, {user: [(item1, rating1), (item2, rating2) ...]}
|
|
77
|
+
ratings = defaultdict(list)
|
|
78
|
+
for (u, v) in test:
|
|
79
|
+
data = self.G.get_edge_data(u,v)
|
|
80
|
+
self.G.remove_edge(u,v)
|
|
81
|
+
ratings[u].append((v, data['rating']))
|
|
82
|
+
|
|
83
|
+
return self.G, ratings
|
|
84
|
+
|
|
85
|
+
def _random_by_ratio(self, p: float, level: str) -> np.array:
|
|
86
|
+
if level == 'global':
|
|
87
|
+
ratings = np.array(list(self.G.get_rating_edges()))
|
|
88
|
+
np.random.shuffle(ratings)
|
|
89
|
+
n_test = math.ceil(ratings.shape[0] * p)
|
|
90
|
+
|
|
91
|
+
return ratings[-n_test:]
|
|
92
|
+
elif level == 'user':
|
|
93
|
+
test = []
|
|
94
|
+
for user, items in self.G.rating_edges.items():
|
|
95
|
+
# Number of test items for user
|
|
96
|
+
items = np.array(list(items))
|
|
97
|
+
np.random.shuffle(items)
|
|
98
|
+
n_test = math.ceil(items.shape[0] * p)
|
|
99
|
+
test += [(user, item) for item in items[-n_test:]]
|
|
100
|
+
|
|
101
|
+
return np.array(test)
|
|
102
|
+
|
|
103
|
+
def _timestamp_by_ratio(self, p: float, level: str) -> np.array:
|
|
104
|
+
if level == 'global':
|
|
105
|
+
ratings = list(self.G.get_rating_edges())
|
|
106
|
+
ratings = self._sort_by_timestamp(ratings)
|
|
107
|
+
ratings = np.array(ratings)
|
|
108
|
+
n_test = math.ceil(ratings.shape[0] * p)
|
|
109
|
+
|
|
110
|
+
return ratings[-n_test:]
|
|
111
|
+
elif level == 'user':
|
|
112
|
+
test = []
|
|
113
|
+
for user, items in self.G.rating_edges.items():
|
|
114
|
+
ratings = [(user, item) for item in items]
|
|
115
|
+
ratings = self._sort_by_timestamp(ratings)
|
|
116
|
+
n_test = math.ceil(len(ratings) * p)
|
|
117
|
+
test += ratings[-n_test:]
|
|
118
|
+
|
|
119
|
+
return np.array(test)
|
|
120
|
+
|
|
121
|
+
def _kfold(self, k : int, level : str):
|
|
122
|
+
G_Copy = deepcopy(self.G)
|
|
123
|
+
|
|
124
|
+
if level == 'global':
|
|
125
|
+
ratings = list(self.G.get_rating_edges())
|
|
126
|
+
ratings = np.array(ratings)
|
|
127
|
+
np.random.shuffle(ratings)
|
|
128
|
+
kfold = KFold(n_splits=k)
|
|
129
|
+
for _, test_index in kfold.split(ratings):
|
|
130
|
+
yield ratings[test_index]
|
|
131
|
+
self.G = deepcopy(G_Copy)
|
|
132
|
+
|
|
133
|
+
elif level == 'user':
|
|
134
|
+
kfold_users, ratings_per_user = [], []
|
|
135
|
+
for user, items in self.G.rating_edges.items():
|
|
136
|
+
items = np.array(list(items))
|
|
137
|
+
np.random.shuffle(items)
|
|
138
|
+
kfold = KFold(n_splits=k).split(items)
|
|
139
|
+
kfold_users.append(kfold)
|
|
140
|
+
edges = np.array([(user, item) for item in items])
|
|
141
|
+
ratings_per_user.append(edges)
|
|
142
|
+
|
|
143
|
+
for _ in range(k):
|
|
144
|
+
test = []
|
|
145
|
+
for idx, kfold in enumerate(kfold_users):
|
|
146
|
+
_, test_index = next(kfold)
|
|
147
|
+
test_ratings = ratings_per_user[idx][test_index].tolist()
|
|
148
|
+
test += test_ratings
|
|
149
|
+
test = np.array(test)
|
|
150
|
+
yield test
|
|
151
|
+
self.G = deepcopy(G_Copy)
|
|
152
|
+
|
|
153
|
+
def _sort_by_timestamp(self, ratings: list) -> list:
|
|
154
|
+
# globally sort the ratings by timestamp
|
|
155
|
+
f = lambda edge: self.G.get_edge_data(edge[0], edge[1])['timestamp']
|
|
156
|
+
return sorted(ratings, key=f)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
File without changes
|