Spaces:
Running on Zero
Running on Zero
| """Download Amazon Reviews 2023 (Clothing_Shoes_and_Jewelry). | |
| 銆愰噸瑕佸彉鏇淬€戝畬鍏ㄦ姏寮?HuggingFace 鏁版嵁闆?API,鏀逛负鐩存帴浠?McAuley Lab 瀹樻柟 | |
| HTTPS 鏈嶅姟鍣ㄦ祦寮忔媺鍙栧師濮?jsonl.gz 鏂囦欢銆傚師鍥? | |
| 1. 鏂扮増 datasets 宸插簾寮?loading script, load_dataset(..., trust_remote_code=True) | |
| 鎶?"Dataset scripts are no longer supported"銆?2. McAuley-Lab/Amazon-Reviews-2023 浠撳簱鐨?main 鍒嗘敮鍙湁鑴氭湰鍜?raw 鐩綍, | |
| parquet 鏂囦欢鍒嗘暎鍦?PR 鍒嗘敮(refs/pr/10)涓?涓?Clothing 绫荤洰骞朵笉瀹屾暣, | |
| 渚濊禆 HF 璧板緱闈炲父鏇叉姌涓斿鏄撹俯鍧戙€?3. 瀹樻柟鏂囨。 (amazon-reviews-2023.github.io) 缁欑殑鐩撮摼鏈€绋? | |
| https://mcauleylab.ucsd.edu/public_datasets/data/amazon_2023/... | |
| 涓€浠?jsonl.gz,娴佸紡 GET + gunzip + readline 涓変欢濂楀嵆鍙€? 瀵?sample 妯″紡灏ゅ叾鍙嬪ソ鈥斺€斾笅澶?N 鏉″氨鎺愭柇杩炴帴,涓嶇敤涓嬪畬鏁翠釜鏂囦欢銆?4. UCSD 鏈嶅姟鍣ㄥ湪鍥藉唴璁块棶閫氬父姣?huggingface.co 绋冲畾 (HF 缁忓父 SNI 闃绘柇)銆? | |
| 銆?026-05 淇銆?- pyarrow 鍐?parquet 鏃朵細鎸夊垪鎺ㄦ柇绫诲瀷, Amazon meta 鐨?price 瀛楁鏄? "鍙兘鏁板瓧 / 鍙兘 '鈥? (em-dash) / 鍙兘 'None' / 鍙兘绌? 鐨勮剰娣峰悎瀛楁, | |
| pyarrow 璇曞浘鎺ㄦ垚 double 鏃堕亣鍒?'鈥? 鐩存帴 ArrowInvalid銆? 瑙e喅: 鍐?parquet 鍓嶇敤 _clean_meta_for_parquet 鎶?price 寮鸿浆 float銆?- 鍚屾椂澧炲姞 jsonl 鍏滃簳鐩? meta 鎷夊畬鍏堝瓨涓€浠藉師濮?jsonl, 鍐嶅啓 parquet銆? 杩欐牱涓囦竴 parquet 鍙堝嚭鍒殑闂, 涓嬫鑳界绾ф仮澶? 涓嶅繀閲嶆柊鐖?25 鍒嗛挓銆?- reviews parquet 濡傛灉宸茬粡瀛樺湪, 璺宠繃 reviews 涓嬭浇闃舵, 鐩存帴澶嶇敤銆?""" | |
| import gzip | |
| import json | |
| import logging | |
| import os | |
| from collections import defaultdict | |
| from typing import Iterable, Iterator | |
| import pandas as pd | |
| import requests | |
| from tqdm import tqdm | |
| from . import config as cfg | |
| logger = logging.getLogger(__name__) | |
| # McAuley Lab 瀹樻柟鐩撮摼妯℃澘 | |
| _BASE = "https://mcauleylab.ucsd.edu/public_datasets/data/amazon_2023/raw" | |
| _REVIEW_URL_TMPL = _BASE + "/review_categories/{cat}.jsonl.gz" | |
| _META_URL_TMPL = _BASE + "/meta_categories/meta_{cat}.jsonl.gz" | |
| def _stream_jsonl_gz(url: str) -> Iterator[dict]: | |
| """Stream a gzipped JSONL file and yield parsed rows. | |
| The function intentionally avoids downloading the full file to disk, so | |
| sample mode can stop early after enough valid records have been collected. | |
| """ | |
| logger.info("GET %s", url) | |
| headers = {"User-Agent": "Mozilla/5.0 (acsa_clothing downloader)"} | |
| with requests.get(url, stream=True, headers=headers, timeout=(15, 60)) as r: | |
| r.raise_for_status() | |
| r.raw.decode_content = False | |
| with gzip.GzipFile(fileobj=r.raw) as gz: | |
| for raw_line in gz: | |
| if not raw_line: | |
| continue | |
| try: | |
| yield json.loads(raw_line) | |
| except json.JSONDecodeError as e: | |
| logger.warning("Skipping one invalid JSON line: %s", e) | |
| continue | |
| def _records_to_df(records: Iterable[dict]) -> pd.DataFrame: | |
| return pd.DataFrame(list(records)) | |
| def _clean_meta_for_parquet(df: pd.DataFrame) -> pd.DataFrame: | |
| """鎶?meta 涓細璁?pyarrow 鎺ㄦ柇绫诲瀷澶辫触鐨勮剰鍊兼竻娲楁帀銆? | |
| Amazon 鍏冩暟鎹噷 price 鏄?"鍙兘鏁板瓧 / 鍙兘 '鈥? / 鍙兘 'None' / 鍙兘绌? 鐨勬贩鍚堝瓧娈? | |
| pandas 榛樿鎺ㄦ垚 object, pyarrow 钀?parquet 鏃跺嵈璇曞浘鎺ㄦ垚 double, | |
| 閬囧埌 '鈥? (em-dash) 杩欑闈炴暟瀛楀瓧绗︿覆灏辨姏 ArrowInvalid銆? 鎵€浠ヨ繖閲屾墜鍔ㄦ妸 price 寮哄埗杞垚 float64, 鏃犳硶瑙f瀽鐨勪竴寰嬪彉 NaN銆? """ | |
| if "price" in df.columns: | |
| df["price"] = pd.to_numeric(df["price"], errors="coerce") | |
| return df | |
| def _backup_jsonl(records: list, parquet_path) -> str: | |
| """鍦ㄥ啓 parquet 鍓嶅厛钀戒竴浠藉師濮?jsonl 鍏滃簳銆? | |
| 璺緞灏辨槸 parquet 鍚屽悕鎹㈠悗缂€銆傚嵆浣垮悗缁?to_parquet 澶辫触涔熶笉蹇呴噸鐖€? """ | |
| raw_jsonl_path = os.path.splitext(str(parquet_path))[0] + ".jsonl" | |
| os.makedirs(os.path.dirname(raw_jsonl_path) or ".", exist_ok=True) | |
| with open(raw_jsonl_path, "w", encoding="utf-8") as f: | |
| for row in records: | |
| f.write(json.dumps(row, ensure_ascii=False) + "\n") | |
| logger.info("Backup jsonl saved to %s (%d rows)", raw_jsonl_path, len(records)) | |
| return raw_jsonl_path | |
| def download_sample(n_reviews: int = 30000, min_text_len: int = 20, | |
| min_reviews_per_product: int = 2, | |
| max_reviews_per_product: int = 5, | |
| scan_multiplier: int = 5, | |
| force: bool = False): | |
| """Sample 妯″紡: 娴佸紡璇诲彇鍓?N 鏉″彲鐢ㄨ瘎璁?鍐嶆媺瀵瑰簲 meta銆? | |
| 'Usable' 鎸? text 闈炵┖涓旈暱搴?>= min_text_len, 涓?parent_asin 闈炵┖銆? """ | |
| review_url = _REVIEW_URL_TMPL.format(cat=cfg.CATEGORY) | |
| meta_url = _META_URL_TMPL.format(cat=cfg.CATEGORY) | |
| # ===== Reviews 闃舵 ===== | |
| # 濡傛灉 reviews parquet 宸插瓨鍦?(涓婃璺戞垚鍔熶簡, 鏄?meta 闃舵宕╃殑), | |
| # 鐩存帴璇诲嚭鏉ュ鐢?seen_asins, 璺宠繃杩欐閲嶆柊涓嬭浇銆? | |
| if os.path.exists(cfg.RAW_REVIEWS_PATH) and not force: | |
| logger.info("Reviews parquet 宸插瓨鍦? 澶嶇敤: %s", cfg.RAW_REVIEWS_PATH) | |
| reviews_df = pd.read_parquet(cfg.RAW_REVIEWS_PATH) | |
| seen_asins = set(reviews_df["parent_asin"].dropna().unique().tolist()) | |
| logger.info("浠庡凡鏈?reviews 涓仮澶?%d 鏉? %d 涓?unique parent_asins", | |
| len(reviews_df), len(seen_asins)) | |
| else: | |
| logger.info( | |
| "Streaming reviews (grouped sample, target=%d, min_per_product=%d, max_per_product=%d, scan_multiplier=%d) from %s ...", | |
| n_reviews, min_reviews_per_product, max_reviews_per_product, scan_multiplier, review_url, | |
| ) | |
| candidate_target = max(n_reviews, n_reviews * max(scan_multiplier, 1)) | |
| candidates = [] | |
| pbar = tqdm(total=candidate_target, desc="review candidates") | |
| for row in _stream_jsonl_gz(review_url): | |
| text = (row.get("text") or "").strip() | |
| if len(text) < min_text_len: | |
| continue | |
| if not row.get("parent_asin"): | |
| continue | |
| candidates.append(row) | |
| pbar.update(1) | |
| if len(candidates) >= candidate_target: | |
| break | |
| pbar.close() | |
| by_asin = defaultdict(list) | |
| for row in candidates: | |
| by_asin[row["parent_asin"]].append(row) | |
| eligible = {asin: rows for asin, rows in by_asin.items() if len(rows) >= min_reviews_per_product} | |
| kept = [] | |
| for asin, rows in sorted(eligible.items(), key=lambda x: len(x[1]), reverse=True): | |
| for row in rows[:max_reviews_per_product]: | |
| kept.append(row) | |
| if len(kept) >= n_reviews: | |
| break | |
| if len(kept) >= n_reviews: | |
| break | |
| if len(kept) < n_reviews: | |
| used = {id(row) for row in kept} | |
| for row in candidates: | |
| if id(row) in used: | |
| continue | |
| kept.append(row) | |
| if len(kept) >= n_reviews: | |
| break | |
| reviews_df = _records_to_df(kept[:n_reviews]) | |
| seen_asins = set(reviews_df["parent_asin"].dropna().unique().tolist()) | |
| per_product = reviews_df.groupby("parent_asin").size() | |
| logger.info( | |
| "Got %d reviews, %d unique parent_asins, avg_reviews_per_product=%.2f, products_with_%d_plus_reviews=%d.", | |
| len(reviews_df), len(seen_asins), len(reviews_df) / max(len(seen_asins), 1), | |
| min_reviews_per_product, int((per_product >= min_reviews_per_product).sum()), | |
| ) | |
| reviews_df.to_parquet(cfg.RAW_REVIEWS_PATH, index=False) | |
| # ===== Meta 闃舵 ===== | |
| # 鍐嶆媺 meta,鍖归厤 parent_asin | |
| # 娉ㄦ剰: meta 娌℃湁淇濊瘉鎺掑簭,鐞嗚涓婅鎵畬鏁翠唤鎵嶈兘纭繚鍛戒腑鎵€鏈?asin銆? | |
| # 浣?meta 姣?reviews 灏忓緱澶?(Clothing meta 鍑犵櫨 MB 鍘嬬缉),瀹屾暣鎵竴閬嶅彲鎺ュ彈銆? logger.info("Streaming meta to match parent_asins from %s ...", meta_url) | |
| meta_kept = [] | |
| remaining = set(seen_asins) | |
| pbar = tqdm(total=len(seen_asins), desc="meta") | |
| for row in _stream_jsonl_gz(meta_url): | |
| asin = row.get("parent_asin") | |
| if asin in remaining: | |
| meta_kept.append(row) | |
| remaining.discard(asin) | |
| pbar.update(1) | |
| if not remaining: | |
| break # 鍏ㄩ儴鍛戒腑,鎻愬墠鏂紑 | |
| pbar.close() | |
| # 1) 鍏堣惤 jsonl 鍏滃簳, 鍝€曚笅闈?to_parquet 鐐镐簡涔熶笉蹇呴噸鐖? | |
| _backup_jsonl(meta_kept, cfg.RAW_META_PATH) | |
| # 2) 鍐嶅仛娓呮礂 + 鍐?parquet | |
| meta_df = _records_to_df(meta_kept) | |
| logger.info( | |
| "Got %d meta entries (matched %.1f%% of review asins).", | |
| len(meta_df), | |
| 100 * len(meta_df) / max(len(seen_asins), 1), | |
| ) | |
| meta_df = _clean_meta_for_parquet(meta_df) | |
| meta_df.to_parquet(cfg.RAW_META_PATH, index=False) | |
| return reviews_df, meta_df | |
| def download_full(): | |
| """Download the full review and metadata tables. Very large; use with care.""" | |
| review_url = _REVIEW_URL_TMPL.format(cat=cfg.CATEGORY) | |
| meta_url = _META_URL_TMPL.format(cat=cfg.CATEGORY) | |
| logger.info("Downloading FULL reviews from %s ...", review_url) | |
| reviews = [] | |
| for row in tqdm(_stream_jsonl_gz(review_url), desc="reviews"): | |
| reviews.append(row) | |
| reviews_df = _records_to_df(reviews) | |
| logger.info("Reviews: %d rows", len(reviews_df)) | |
| reviews_df.to_parquet(cfg.RAW_REVIEWS_PATH, index=False) | |
| logger.info("Downloading FULL meta from %s ...", meta_url) | |
| metas = [] | |
| for row in tqdm(_stream_jsonl_gz(meta_url), desc="meta"): | |
| metas.append(row) | |
| # 钀?jsonl 鍏滃簳 | |
| _backup_jsonl(metas, cfg.RAW_META_PATH) | |
| meta_df = _records_to_df(metas) | |
| logger.info("Meta: %d rows", len(meta_df)) | |
| meta_df = _clean_meta_for_parquet(meta_df) | |
| meta_df.to_parquet(cfg.RAW_META_PATH, index=False) | |
| return reviews_df, meta_df | |