1
1
2026-06-04 3ee8742df6bc2da18b159d9ea769f7dd44e062d3
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
"""Small CSV profiling helper for Project F experiment dry-run."""
 
from __future__ import annotations
 
import argparse
import csv
import json
from collections import Counter
from pathlib import Path
from typing import Any
 
 
def _read_rows(input_path: Path) -> tuple[list[str], list[dict[str, str]]]:
    with input_path.open("r", encoding="utf-8-sig", newline="") as fh:
        reader = csv.DictReader(fh)
        if not reader.fieldnames:
            raise ValueError("input CSV has no header")
        rows = [dict(row) for row in reader]
        return list(reader.fieldnames), rows
 
 
def build_profile(input_path: Path) -> dict[str, Any]:
    columns, rows = _read_rows(input_path)
    missing_by_column: dict[str, int] = {}
    for column in columns:
        missing_by_column[column] = sum(1 for row in rows if not (row.get(column) or "").strip())
 
    id_values = [(row.get("id") or "").strip() for row in rows]
    nonempty_ids = [value for value in id_values if value]
    duplicate_id_count = len(nonempty_ids) - len(set(nonempty_ids))
 
    group_counts: dict[str, int] = {}
    if "group" in columns:
        group_counts = dict(sorted(Counter((row.get("group") or "").strip() or "MISSING" for row in rows).items()))
 
    quality_pass = bool(rows) and duplicate_id_count == 0 and all(
        missing_by_column.get(column, 0) == 0 for column in ("id", "group", "value", "event_date") if column in columns
    )
 
    return {
        "input_path": str(input_path),
        "row_count": len(rows),
        "column_count": len(columns),
        "columns": columns,
        "missing_by_column": missing_by_column,
        "duplicate_id_count": duplicate_id_count,
        "group_counts": group_counts,
        "quality_pass": quality_pass,
    }
 
 
def write_outputs(profile: dict[str, Any], output_dir: Path) -> None:
    output_dir.mkdir(parents=True, exist_ok=True)
 
    (output_dir / "summary.json").write_text(
        json.dumps(profile, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
 
    with (output_dir / "column_quality.csv").open("w", encoding="utf-8", newline="") as fh:
        writer = csv.DictWriter(fh, fieldnames=["column", "missing_count"])
        writer.writeheader()
        for column, missing_count in profile["missing_by_column"].items():
            writer.writerow({"column": column, "missing_count": missing_count})
 
    readout = [
        "# CSV Profile Readout",
        "",
        "创建人员:csv_profile_experiment.py",
        "文件职责:记录 RUN-F-DEV-001 的 CSV 质量统计摘要。",
        "管理规范/模板:exp-doc/实验存储体系.md;exp-doc/实验设计.md。",
        "引用文件:summary.json;column_quality.csv;exp-data/raw/EXP-F-DEV-001/input_sample.csv。",
        "记录方式:实验结果包 readout;由 helper 运行时生成。",
        "",
        f"row_count: {profile['row_count']}",
        f"column_count: {profile['column_count']}",
        f"duplicate_id_count: {profile['duplicate_id_count']}",
        f"quality_pass: {str(profile['quality_pass']).lower()}",
        "",
        "group_counts:",
    ]
    for group, count in profile["group_counts"].items():
        readout.append(f"- {group}: {count}")
    (output_dir / "readout.md").write_text("\n".join(readout) + "\n", encoding="utf-8")
 
 
def main() -> int:
    parser = argparse.ArgumentParser(description="Profile a CSV and write a small experiment result package.")
    parser.add_argument("--input", required=True, type=Path)
    parser.add_argument("--output-dir", required=True, type=Path)
    args = parser.parse_args()
 
    profile = build_profile(args.input)
    write_outputs(profile, args.output_dir)
    return 0
 
 
if __name__ == "__main__":
    raise SystemExit(main())