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
342 lines
11 KiB
Python
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()
|