Files
wehub-resource-sync 593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

342 lines
11 KiB
Python

#!/usr/bin/env python3
"""Probe HuggingFace datasets, auto-generate YAML configs, and produce a smoke-test report.
Usage:
python scripts/probe_hf_datasets.py [--start N] [--end N] [--resume]
The script:
1. Streams 20 rows from each candidate dataset (no full download)
2. Inspects column types
3. Auto-generates a YAML config for simple datasets
4. Writes results to scripts/probe_results.json
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import traceback
from collections import Counter
from typing import Any
import datasets as hf_datasets
import pandas as pd
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
CONFIGS_DIR = os.path.join(os.path.dirname(__file__), "..", "ludwig", "datasets", "configs")
RESULTS_PATH = os.path.join(os.path.dirname(__file__), "probe_results.json")
CANDIDATES_PATH = os.path.join(os.path.dirname(__file__), "hf_dataset_candidates.json")
EXISTING = {p.replace(".yaml", "") for p in os.listdir(CONFIGS_DIR) if p.endswith(".yaml")}
LUDWIG_TYPES = {"text", "category", "binary", "number", "image", "audio", "sequence", "set", "vector", "bag"}
_OUTPUT_COL_CANDIDATES = [
"label",
"labels",
"target",
"output",
"class",
"category",
"sentiment",
"answer",
"score",
"rating",
"tag",
"tags",
"intent",
"emotion",
"polarity",
"stance",
"result",
"verdict",
"quality",
]
# ── Column-type inference ────────────────────────────────────────────────────
def infer_ludwig_type(col: pd.Series, feature_info=None) -> str:
dtype = col.dtype
if feature_info is not None:
fname = str(type(feature_info).__name__)
if "ClassLabel" in fname:
return "category"
if "Sequence" in fname:
return "_list"
if "Image" in fname:
return "image"
if "Audio" in fname:
return "audio"
if "Translation" in fname:
return "_translation"
if dtype is object:
sample = col.dropna().head(20)
if len(sample) == 0:
return "text"
first = sample.iloc[0]
if isinstance(first, (list, tuple)):
return "_list"
if isinstance(first, dict):
return "_dict"
if isinstance(first, (bytes, bytearray)):
return "_bytes"
unique_ratio = col.nunique() / max(len(col), 1)
avg_len = sample.apply(lambda x: len(str(x))).mean()
if unique_ratio < 0.05 and avg_len < 50:
return "category"
return "text"
if pd.api.types.is_bool_dtype(dtype):
return "binary"
if pd.api.types.is_integer_dtype(dtype):
return "category" if col.nunique() <= 20 else "number"
if pd.api.types.is_float_dtype(dtype):
return "number"
return "text"
def infer_columns(df: pd.DataFrame, hf_features) -> dict[str, str]:
result = {}
for col in df.columns:
if col == "split":
continue
fi = hf_features.get(col) if hf_features else None
result[col] = infer_ludwig_type(df[col], fi)
return result
# ── Output column resolution ─────────────────────────────────────────────────
def _resolve_output_cols(entry: dict, columns: dict[str, str]) -> list[str] | None:
"""Handle both column-name and type-name forms for output_features."""
raw = entry.get("output_features", [])
if not raw:
return None
col_names = set(columns.keys())
# If all values are actual column names, use them directly
if all(oc in col_names for oc in raw):
return raw
# Agent returned types ("category", "binary") not column names — auto-detect
for cand in _OUTPUT_COL_CANDIDATES:
if cand in col_names and not columns[cand].startswith("_"):
return [cand]
# Fallback: first column with category/binary/number type that isn't an id
for col, typ in columns.items():
if col in ("idx", "id", "index") or typ.startswith("_"):
continue
if typ in ("category", "binary", "number"):
return [col]
return None
# ── YAML generation ──────────────────────────────────────────────────────────
YAML_TMPL = """\
version: 1.0
name: {name}
huggingface_dataset_id: {hf_id}
{subsample_line}loader: hugging_face.HFLoader
description: |
{description}
columns:
{columns_block}
output_features:
{output_block}
"""
def make_yaml(entry: dict, columns: dict[str, str]) -> tuple[str | None, list[str] | None]:
"""Return (yaml_string, output_cols) or (None, None)."""
for t in columns.values():
if t.startswith("_"):
return None, None
output_cols = _resolve_output_cols(entry, columns)
if not output_cols:
return None, None
for oc in output_cols:
if oc not in columns:
return None, None
subsample = entry.get("hf_subsample")
subsample_line = f"huggingface_subsample: {subsample}\n" if subsample else ""
columns_block = "\n".join(
f" - name: {col}\n type: {typ}" for col, typ in columns.items() if not col.startswith("_")
)
output_block = "\n".join(f" - name: {oc}\n type: {columns[oc]}" for oc in output_cols if oc in columns)
notes = entry.get("notes", "") or entry.get("name", "")
yaml_str = YAML_TMPL.format(
name=entry["name"],
hf_id=entry["hf_id"],
subsample_line=subsample_line,
description=notes,
columns_block=columns_block,
output_block=output_block,
)
return yaml_str, output_cols
# ── Dataset probing ──────────────────────────────────────────────────────────
def probe_one(entry: dict) -> dict[str, Any]:
hf_id = entry["hf_id"]
hf_sub = entry.get("hf_subsample")
name = entry["name"]
result: dict[str, Any] = {
"name": name,
"hf_id": hf_id,
"hf_subsample": hf_sub,
"task": entry.get("task", ""),
"status": "unknown",
"columns": {},
"rows": -1,
"splits": [],
"yaml_written": False,
"output_cols": [],
"error": None,
"needs_custom_loader": entry.get("needs_custom_loader", False),
}
if name in EXISTING:
result["status"] = "already_exists"
return result
try:
ds_stream = hf_datasets.load_dataset(
path=hf_id,
name=hf_sub,
trust_remote_code=False,
streaming=True,
)
split_name = "train" if "train" in ds_stream else list(ds_stream.keys())[0]
ds = ds_stream[split_name]
rows = list(ds.take(20))
if not rows:
result["status"] = "error"
result["error"] = "No rows returned"
return result
df = pd.DataFrame(rows)
hf_features = ds.features if hasattr(ds, "features") else None
columns = infer_columns(df, hf_features)
result["columns"] = columns
result["splits"] = list(ds_stream.keys())
has_complex = any(t.startswith("_") for t in columns.values())
result["needs_custom_loader"] = has_complex or entry.get("needs_custom_loader", False)
yaml_str, output_cols = make_yaml(entry, columns)
if yaml_str and not result["needs_custom_loader"]:
out_path = os.path.join(CONFIGS_DIR, f"{name}.yaml")
with open(out_path, "w") as f:
f.write(yaml_str)
result["yaml_written"] = True
result["output_cols"] = output_cols
result["status"] = "auto_generated"
elif result["needs_custom_loader"]:
result["status"] = "needs_custom_loader"
else:
result["status"] = "skipped_no_yaml"
# Store why: which output cols were tried
result["debug_output_cols"] = _resolve_output_cols(entry, columns)
except Exception as e:
result["status"] = "error"
result["error"] = f"{type(e).__name__}: {e}"
logger.debug(traceback.format_exc())
return result
# ── Main ─────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--start", type=int, default=0)
parser.add_argument("--end", type=int, default=None)
parser.add_argument("--resume", action="store_true")
args = parser.parse_args()
with open(CANDIDATES_PATH) as f:
candidates = json.load(f)
candidates = candidates[args.start : args.end]
existing_results: dict[str, dict] = {}
if args.resume and os.path.exists(RESULTS_PATH):
try:
with open(RESULTS_PATH) as f:
for r in json.load(f):
existing_results[r["name"]] = r
except Exception:
pass
results = []
total = len(candidates)
for i, entry in enumerate(candidates):
name = entry["name"]
if args.resume and name in existing_results:
results.append(existing_results[name])
print(f"[{i + 1}/{total}] SKIP (resumed) {name}")
continue
print(f"[{i + 1}/{total}] Probing {name} ({entry['hf_id']})...", end=" ", flush=True)
r = probe_one(entry)
results.append(r)
sym = {
"auto_generated": "✓",
"already_exists": "=",
"needs_custom_loader": "~",
"error": "✗",
"skipped_no_yaml": "?",
}.get(r["status"], "?")
print(f"{sym} [{r['status']}]")
if r["error"]:
print(f" ERROR: {r['error'][:100]}")
# Merge with any resumed results and save
all_results = {**existing_results, **{rr["name"]: rr for rr in results}}
with open(RESULTS_PATH, "w") as f:
json.dump(list(all_results.values()), f, indent=2)
status_counts = Counter(r["status"] for r in results)
print("\n=== Summary ===")
for status, count in status_counts.most_common():
print(f" {status}: {count}")
print(f" Total: {len(results)}")
print("\nAuto-generated:")
for r in results:
if r["status"] == "auto_generated":
print(f" {r['name']} → output: {r.get('output_cols')}")
print("\nNeeds custom loader:")
for r in results:
if r["status"] == "needs_custom_loader":
complex_cols = [c for c, t in r["columns"].items() if t.startswith("_")]
print(f" {r['name']} ({r['task']}): complex={complex_cols}")
print("\nErrors:")
for r in results:
if r["status"] == "error":
print(f" {r['name']}: {r['error'][:80]}")
if __name__ == "__main__":
main()