Skip to content

Commit 3ba3a91

Browse files
authored
Global temporal and leave-last-out splitting for next-item evaluation (#708)
* global temporal and leave-last-out splitting for next-item evaluation * add Amazon Review dataset support * expand test coverage for NextItemEvaluation * docs: add Amazon dataset documentation
1 parent 54b0336 commit 3ba3a91

8 files changed

Lines changed: 779 additions & 2 deletions

File tree

cornac/data/dataset.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,6 +1203,20 @@ def build(
12031203
extra_pos = ts_pos + 1
12041204
extra_data = [data[i][extra_pos] for i in valid_idx] if fmt in ["SITJson", "USITJson"] else None
12051205

1206+
if timestamps is not None and len(timestamps) > 1:
1207+
order = np.argsort(session_indices, kind="stable")
1208+
s = session_indices[order]
1209+
t = timestamps[order]
1210+
decreasing = (t[1:] < t[:-1]) & (s[1:] == s[:-1])
1211+
if decreasing.any():
1212+
n_bad = int(decreasing.sum())
1213+
warnings.warn(
1214+
f"{n_bad} interaction(s) are not in chronological order within "
1215+
"their session. Sequential models treat input row order as the "
1216+
"ground-truth sequence; sort your data by (session, timestamp) "
1217+
"before building the dataset."
1218+
)
1219+
12061220
dataset = cls(
12071221
num_users=len(global_uid_map),
12081222
num_sessions=len(set(session_indices)),

cornac/datasets/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,25 @@ Session-aware recommendation extends next-item (session-based) recommendation by
281281
| [Cosmetics](./cosmetics.py) | 17,268 | 42,367 | 172,242 | 2,533,262 | 9.97 | 59.79 | 14.71 | 0.346% |
282282

283283
For session-based (next-item) evaluation, [Diginetica](./diginetica.py)'s `load_val()` and `load_test()` default to `mode="session-based"`, returning each user's single held-out session (`val_sbr`/`test_sbr`) with no training transitions repeated — the clean evaluation set used by session-based models such as [FPMC](../models/fpmc/) and [GRU4Rec](../models/gru4rec/). Pass `mode="session-aware"` to load the cumulative files (`val`/`test`) instead, where each user's prior sessions precede their held-out one for cross-session models.
284+
285+
---
286+
287+
## Semantic-ID Datasets
288+
### Amazon Product Review
289+
[Amazon Product Review](./amazon_review.py) 5-core
290+
291+
Each user's reviews form one chronologically-ordered sequence. Interactions are loaded via `amazon_review.load_feedback(category=...)` in `UIRT` format (user, item, rating, timestamp). No preprocessing is needed and the data is kept as-is for comparability with published results (with `leave-last-out` split).
292+
293+
| Dataset | #Users | #Items | #Interactions | Type |
294+
| :----------------------- | -----: | -----: | ------------: | :-------- |
295+
| Amazon Beauty (`beauty`) | 22,363 | 12,101 | 198,502 | INT [1,5] |
296+
| Amazon Sports (`sports`) | 35,598 | 18,357 | 296,337 | INT [1,5] |
297+
| Amazon Toys (`toys`) | 19,412 | 11,924 | 167,597 | INT [1,5] |
298+
299+
```Python
300+
from cornac.datasets import amazon_review
301+
from cornac.eval_methods import NextItemEvaluation
302+
303+
data = amazon_review.load_feedback(category="beauty") # UIRT tuples, chronological per user
304+
eval_method = NextItemEvaluation.leave_last_out(data, fmt="UIRT")
305+
```

cornac/datasets/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from . import amazon_clothing
1717
from . import amazon_digital_music
1818
from . import amazon_office
19+
from . import amazon_review
1920
from . import amazon_toy
2021
from . import citeulike
2122
from . import cosmetics

cornac/datasets/amazon_review.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Copyright 2026 The Cornac Authors. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
# ============================================================================
15+
"""
16+
Amazon Product Review datasets.
17+
18+
There are three versions: '2014', '2018', and '2023' available.
19+
'2014' is the version used in the Semantic-ID literature (e.g., TIGER).
20+
21+
Source: https://cseweb.ucsd.edu/~jmcauley/datasets/amazon/links.html
22+
"""
23+
24+
import gzip
25+
import json
26+
import os
27+
from typing import List
28+
29+
from ..data import Reader
30+
from ..utils import cache
31+
32+
# category -> reviews_<cat>_5.json.gz
33+
_CATEGORY_FILES = {
34+
"beauty": "Beauty",
35+
"sports": "Sports_and_Outdoors",
36+
"toys": "Toys_and_Games",
37+
}
38+
39+
_BASE_URL = "https://snap.stanford.edu/data/amazon/productGraph/categoryFiles"
40+
41+
42+
def _preprocess(gz_path: str, csv_path: str) -> None:
43+
"""Parse the raw 5-core reviews into ``user,item,rating,timestamp`` rows.
44+
45+
Only mechanical cleaning is applied (drop rows with a missing field);
46+
rows are sorted chronologically per user so downstream sequential builders
47+
receive time-ordered sessions.
48+
"""
49+
rows = []
50+
with gzip.open(gz_path, "rt", encoding="utf-8") as f:
51+
for line in f:
52+
r = json.loads(line)
53+
user = r.get("reviewerID")
54+
item = r.get("asin")
55+
rating = r.get("overall")
56+
timestamp = r.get("unixReviewTime")
57+
if user is None or item is None or rating is None or timestamp is None:
58+
continue
59+
rows.append((user, item, float(rating), int(timestamp)))
60+
61+
rows.sort(key=lambda x: (x[0], x[3])) # (user, timestamp)
62+
63+
with open(csv_path, "w") as f:
64+
for user, item, rating, timestamp in rows:
65+
f.write(f"{user},{item},{rating},{timestamp}\n")
66+
67+
68+
def load_feedback(category: str, version: str = "2014", fmt: str = "UIRT", reader: Reader = None) -> List:
69+
"""Load the user-item review feedback, chronologically ordered per user.
70+
71+
Parameters
72+
----------
73+
category: str, required
74+
One of ``'beauty'``, ``'sports'``, ``'toys'`` -- the three categories
75+
used by TIGER and subsequent Semantic-ID papers.
76+
77+
version: str, default: '2014'
78+
Dataset version. Only ``'2014'`` (McAuley 5-core) is currently supported;
79+
2018 and 2023 are available, but 2014 is the version used throughout the Semantic-ID literature.
80+
81+
fmt: str, default: 'UIRT'
82+
Data format; the underlying file has user, item, rating, and timestamp
83+
columns, so ``'UIR'`` and ``'UI'`` are also valid.
84+
85+
reader: `obj:cornac.data.Reader`, default: None
86+
Reader object used to read the data.
87+
88+
Returns
89+
-------
90+
data: array-like
91+
Data in the form of a list of tuples (user, item, rating, timestamp).
92+
"""
93+
if category not in _CATEGORY_FILES:
94+
raise ValueError(f"category='{category}' not supported; " f"choose one of {sorted(_CATEGORY_FILES)}")
95+
if version != "2014":
96+
raise ValueError(f"version='{version}' not supported; only '2014' (McAuley 5-core) " "is available")
97+
98+
stem = _CATEGORY_FILES[category]
99+
gz_path = cache(
100+
url=f"{_BASE_URL}/reviews_{stem}_5.json.gz",
101+
relative_path=f"amazon_review/{category}_{version}.json.gz",
102+
)
103+
csv_path = f"{gz_path[:-len('.json.gz')]}.csv"
104+
if not os.path.exists(csv_path):
105+
_preprocess(gz_path, csv_path)
106+
107+
reader = Reader() if reader is None else reader
108+
return reader.read(csv_path, fmt=fmt, sep=",")

0 commit comments

Comments
 (0)