Spaces:
Running on Zero
Running on Zero
File size: 6,755 Bytes
99bc6f9 2f15b93 71f4e90 2f15b93 71f4e90 2f15b93 71f4e90 2f15b93 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | """
Loader for the BookRec recommender on the Goodbooks-10k dataset.
Loads the COMPLETE real dataset:
books.csv -> 10,000 books (title, authors, isbn, live image_url)
ratings.csv -> 5,976,479 explicit 1-5 ratings (user_id, book_id, rating)
book_tags.csv -> book -> tag_id with counts (the genre/content signal)
tags.csv -> tag_id -> tag_name
Design decisions handled here:
* Item key is ISBN = str(book_id), so the pipeline keys on one string.
* Genres are the top tags per book (shelf-labelled stop-tags removed).
* Covers prefer the live Goodreads (gr-assets) https URL, with an Open
Library fallback by ISBN, so a cover URL is always available.
"""
from __future__ import annotations
from pathlib import Path
from typing import Optional, Tuple
import numpy as np
import pandas as pd
DATA_DIR = Path(__file__).resolve().parents[1] / "data"
# ---------------------------------------------------------------------------
# Covers (resilient)
# ---------------------------------------------------------------------------
def cover_url(isbn: str, size: str = "M") -> str:
"""Best-effort cover URL: Open Library API (free, no key, live)."""
isbn = str(isbn).strip()
if not isbn:
return ""
size_map = {"s": "S", "m": "M", "l": "L"}
letter = size_map.get(str(size).lower(), "M")
return f"https://covers.openlibrary.org/b/isbn/{isbn}-{letter}.jpg"
def pick_cover_url(row: pd.Series) -> str:
"""Return the best available cover URL for a book row.
Prefers a live HTTPS cover (Goodreads gr-assets), else Open Library by
ISBN.
"""
for col in ("Image-URL-M", "Image-URL-L", "Image-URL-S"):
v = row.get(col)
if isinstance(v, str) and v.startswith("https://"):
return v
isbn = str(row.get("ISBN", "")).strip()
if isbn:
return cover_url(isbn, "M")
return ""
def isbn_cover(isbn: str) -> str:
return cover_url(isbn, "M")
# ---------------------------------------------------------------------------
# Goodbooks-10k loader (ratings + genres/tags + live covers)
# ---------------------------------------------------------------------------
GB_STOP_TAGS = {"to-read", "currently-reading", "favorites", "books-i-own",
"read", "owned", "default", "re-read", "favorites2"}
def load_goodbooks() -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame,
pd.DataFrame]:
"""Load the four Goodbooks-10k raw files."""
d = DATA_DIR
books = pd.read_csv(d / "books.csv")
ratings = pd.read_csv(d / "ratings.csv")
book_tags = pd.read_csv(d / "book_tags.csv")
tags = pd.read_csv(d / "tags.csv")
return books, ratings, book_tags, tags
def build_genres(books, book_tags, tags, top_k: int = 15) -> pd.Series:
"""Map each goodreads_book_id to a space-joined string of its top genres.
Joins book_tags -> tags, drops shelf-labelled stop-tags, keeps the top-K
highest-count genre tags per book as a content string.
"""
m = book_tags.merge(tags, on="tag_id")
m = m[~m["tag_name"].astype(str).isin(GB_STOP_TAGS)]
def top(db):
g = db.sort_values("count", ascending=False).head(top_k)
return " ".join(g["tag_name"].astype(str))
genres = m.groupby("goodreads_book_id").apply(top, include_groups=False)
genres = genres.rename("genres_text")
return genres
def prepare_goodbooks() -> dict:
"""Load Goodbooks-10k and normalise to the pipeline's internal schema.
Returns books/ratings with the standard columns the recommender expects
(ISBN = str(book_id), Book-Title, Book-Author, Image-URL-M, Book-Genres)
plus a genres-text channel for content similarity.
"""
books, rat, book_tags, tags = load_goodbooks()
genres = build_genres(books, book_tags, tags, top_k=15)
books = books.copy()
books["ISBN"] = books["book_id"].astype(str)
books["Book-Title"] = books["title"]
books["Book-Author"] = books["authors"]
books["Image-URL-M"] = books["image_url"]
books["Book-Genres"] = (books["goodreads_book_id"].map(genres)
.fillna(""))
rat = rat.rename(columns={"user_id": "User-ID", "book_id": "ISBN",
"rating": "Book-Rating"})
rat["User-ID"] = rat["User-ID"].astype(str)
rat["ISBN"] = rat["ISBN"].astype(str)
return {
"books": books.reset_index(drop=True),
"ratings": rat.reset_index(drop=True),
"n_books": len(books),
"n_ratings": len(rat),
"n_users": rat["User-ID"].nunique(),
"explicit_share": 1.0, # Goodbooks ratings are all explicit (1-5)
"implicit_share": 0.0,
"source": "goodbooks-10k",
}
def prepare_amazon() -> dict:
"""Load the sampled Amazon Books parquets into the pipeline schema.
Reads amazon_books/books.parquet and amazon_books/ratings.parquet
(converted from the McAuley-Lab/Amazon-Reviews-2023 benchmark) and maps
them to the standard ISBN / Book-Title / Book-Author / Book-Genres /
User-ID / Book-Rating columns the recommender expects.
"""
import pandas as pd
base = Path(__file__).resolve().parents[1] / "amazon_books"
books = pd.read_parquet(base / "books.parquet")
rat = pd.read_parquet(base / "ratings.parquet")
def _flatten(v):
if v is None or isinstance(v, str) or not hasattr(v, "__iter__"):
return [v]
out = []
for x in v:
out.extend(_flatten(x))
return out
def _first_author(a) -> str:
if a is None or isinstance(a, str):
return "" if a is None else a
flat = _flatten(a)
return str(flat[0]) if flat else ""
def _genres(c) -> str:
flat = _flatten(c)
cleaned = [str(x) for x in flat if str(x).lower() != "books"]
return " ".join(dict.fromkeys(cleaned))
books = books.copy()
books["ISBN"] = books["item_id"].astype(str)
books["Book-Title"] = books["title"].fillna("")
books["Book-Author"] = (books["authors"].map(_first_author)
.fillna(""))
books["Book-Genres"] = (books["categories"].map(_genres)
.fillna(""))
rat = rat.rename(columns={"user_id": "User-ID", "item_id": "ISBN",
"rating": "Book-Rating"})
rat["User-ID"] = rat["User-ID"].astype(str)
rat["ISBN"] = rat["ISBN"].astype(str)
return {
"books": books.reset_index(drop=True),
"ratings": rat.reset_index(drop=True),
"n_books": len(books),
"n_ratings": len(rat),
"n_users": rat["User-ID"].nunique(),
"explicit_share": 1.0,
"implicit_share": 0.0,
"source": "amazon-books-5core",
}
|