| """Pull the astro-ph text slice out of ArxivCap via DuckDB (no image download).""" |
| import argparse |
| import os |
| import time |
| from collections import Counter |
| from pathlib import Path |
|
|
| import duckdb |
|
|
| SRC = "hf://datasets/MMInstruction/ArxivCap/data/*.parquet" |
|
|
| EXTRACT = """ |
| SELECT |
| arxiv_id, |
| title, |
| abstract, |
| meta.meta_from_kaggle.categories AS categories, |
| meta.meta_from_s2.citationCount AS citations, |
| list_transform(caption_images, x -> x.caption) AS captions, |
| list_transform( |
| caption_images, |
| x -> substr( |
| array_to_string( |
| flatten(list_transform(x.cil_pairs, y -> y.image_ocr)), ' ' |
| ), 1, 500) |
| ) AS ocr |
| FROM read_parquet('{src}') |
| WHERE contains(meta.meta_from_kaggle.categories, 'astro-ph') |
| {limit} |
| """ |
|
|
|
|
| def year_from_arxiv_id(arxiv_id): |
| try: |
| yy = int(arxiv_id.split("/")[1][:2]) if "/" in arxiv_id else int(arxiv_id[:2]) |
| return 1900 + yy if yy > 80 else 2000 + yy |
| except (ValueError, IndexError): |
| return None |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--src", default=SRC) |
| ap.add_argument("--out", default="astro_captions.parquet") |
| ap.add_argument("--limit", type=int, default=None) |
| ap.add_argument("--parts-dir", default="census_parts") |
| args = ap.parse_args() |
|
|
| con = duckdb.connect() |
| for setting in ["SET enable_progress_bar = true", "SET http_retries = 8", |
| "SET http_retry_wait_ms = 2000", "SET http_retry_backoff = 2", |
| "SET threads = 2"]: |
| try: |
| con.execute(setting) |
| except duckdb.Error: |
| pass |
|
|
| token = os.environ.get("HF_TOKEN", "") |
| if token: |
| con.execute(f"CREATE SECRET hf (TYPE HUGGINGFACE, TOKEN '{token}')") |
| else: |
| print("WARNING: no HF_TOKEN set, anonymous requests get rate limited fast.") |
|
|
| print("Extracting astro-ph slice (text columns only)...") |
| if args.limit or not args.src.startswith("hf://"): |
| limit = f"LIMIT {args.limit}" if args.limit else "" |
| con.execute(f"COPY ({EXTRACT.format(src=args.src, limit=limit)}) " |
| f"TO '{args.out}' (FORMAT PARQUET)") |
| else: |
| parts = Path(args.parts_dir) |
| parts.mkdir(exist_ok=True) |
| chunks = [f"{y:02d}" for y in list(range(91, 100)) + list(range(0, 24))] |
| for yy in chunks: |
| part_file = parts / f"astro_{yy}.parquet" |
| if part_file.exists(): |
| continue |
| chunk_src = args.src.replace("*", f"arXiv_src_{yy}*") |
| query = EXTRACT.format(src=chunk_src, limit="") |
| for attempt in range(5): |
| try: |
| con.execute(f"COPY ({query}) TO '{part_file}' (FORMAT PARQUET)") |
| print(f" {yy}: done") |
| break |
| except duckdb.Error as e: |
| msg = str(e) |
| if "No files found" in msg: |
| print(f" {yy}: no files, skipping") |
| break |
| if attempt == 4: |
| raise |
| wait = 30 * (attempt + 1) |
| print(f" {yy}: {msg[:80]} -- retrying in {wait}s") |
| time.sleep(wait) |
| con.execute(f"COPY (SELECT * FROM read_parquet('{parts}/astro_*.parquet')) " |
| f"TO '{args.out}' (FORMAT PARQUET)") |
|
|
| papers, figures, usable, fpp, cap_chars = con.execute(f""" |
| SELECT count(*), |
| sum(len(captions)), |
| sum(len(list_filter(captions, c -> length(c) >= 20))), |
| avg(len(captions)), |
| round(avg(list_aggregate(list_transform(captions, c -> length(c)), 'avg')), 0) |
| FROM read_parquet('{args.out}') |
| """).fetchone() |
|
|
| print(f"\nPapers: {papers:,} Figures: {int(figures):,} " |
| f"Usable captions: {int(usable):,} Figs/paper: {fpp:.1f} " |
| f"Mean caption: {int(cap_chars)} chars") |
|
|
| ids = [r[0] for r in con.execute( |
| f"SELECT arxiv_id FROM read_parquet('{args.out}')").fetchall()] |
| by_year = Counter(y for y in (year_from_arxiv_id(i) for i in ids) if y) |
| for y in sorted(by_year): |
| print(f" {y}: {by_year[y]:,}") |
| print(f"\nSlice written to {args.out}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|