mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-15 13:36:35 +03:00
* feat(skill): add actuarial risk modeling methodology * fix(skill): refresh generated catalogs
101 lines
4.2 KiB
Python
101 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Read-only profiling for CSV and JSONL risk-modeling inputs."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import math
|
|
import re
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Iterable, List, Optional
|
|
|
|
MISSING = {"", "na", "n/a", "null", "none", "nan", "missing"}
|
|
NUMBER = re.compile(r"^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$")
|
|
|
|
|
|
def rows_from(path: Path, limit: Optional[int]) -> Iterable[Dict[str, str]]:
|
|
with path.open(newline="", encoding="utf-8-sig") as handle:
|
|
if path.suffix.lower() in {".jsonl", ".ndjson"}:
|
|
for i, line in enumerate(handle):
|
|
if limit is not None and i >= limit:
|
|
break
|
|
if line.strip():
|
|
value = json.loads(line)
|
|
if not isinstance(value, dict):
|
|
raise ValueError(f"JSONL line {i + 1} is not an object")
|
|
yield {str(k): "" if v is None else str(v) for k, v in value.items()}
|
|
else:
|
|
reader = csv.DictReader(handle)
|
|
if reader.fieldnames is None:
|
|
raise ValueError("CSV has no header")
|
|
for i, row in enumerate(reader):
|
|
if limit is not None and i >= limit:
|
|
break
|
|
yield {str(k): "" if v is None else str(v) for k, v in row.items()}
|
|
|
|
|
|
def numeric_summary(values: List[float]) -> Dict[str, Any]:
|
|
values = sorted(values)
|
|
n = len(values)
|
|
def q(p: float) -> float:
|
|
if n == 1:
|
|
return values[0]
|
|
pos = (n - 1) * p
|
|
lo, hi = math.floor(pos), math.ceil(pos)
|
|
return values[lo] + (values[hi] - values[lo]) * (pos - lo)
|
|
mean = sum(values) / n
|
|
variance = sum((x - mean) ** 2 for x in values) / max(1, n - 1)
|
|
sd = math.sqrt(variance)
|
|
skew = (sum((x - mean) ** 3 for x in values) / n) / (sd ** 3) if sd else None
|
|
return {"n": n, "min": values[0], "q25": q(.25), "median": q(.5), "q75": q(.75), "max": values[-1], "mean": mean, "sd": sd, "skewness_rough": skew}
|
|
|
|
|
|
def profile(path: Path, limit: Optional[int]) -> Dict[str, Any]:
|
|
rows = list(rows_from(path, limit))
|
|
fields = sorted({key for row in rows for key in row})
|
|
columns: Dict[str, Any] = {}
|
|
for field in fields:
|
|
raw = [row.get(field, "") for row in rows]
|
|
missing = sum(value.strip().lower() in MISSING for value in raw)
|
|
candidates = [float(value) for value in raw if value.strip() and NUMBER.fullmatch(value.strip())]
|
|
numeric = len(candidates) == len(raw) - missing and bool(candidates)
|
|
item: Dict[str, Any] = {"rows": len(raw), "missing": missing, "missing_rate": missing / len(raw) if raw else 0.0, "unique_nonmissing": len(set(value for value in raw if value.strip().lower() not in MISSING))}
|
|
if numeric:
|
|
item["type"] = "numeric"
|
|
item["numeric"] = numeric_summary(candidates)
|
|
item["zero_count"] = sum(value == 0 for value in candidates)
|
|
item["negative_count"] = sum(value < 0 for value in candidates)
|
|
item["positive_count"] = sum(value > 0 for value in candidates)
|
|
else:
|
|
item["type"] = "text"
|
|
counts = Counter(value for value in raw if value.strip().lower() not in MISSING)
|
|
item["top_values"] = [{"value": value, "count": count} for value, count in counts.most_common(10)]
|
|
columns[field] = item
|
|
return {"source": str(path), "rows_profiled": len(rows), "row_limit": limit, "columns": columns}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("input", type=Path)
|
|
parser.add_argument("--limit", type=int, default=10000)
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
result = profile(args.input, args.limit)
|
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
print(f"error: {exc}", file=sys.stderr)
|
|
return 2
|
|
rendered = json.dumps(result, indent=2, sort_keys=True)
|
|
if args.output:
|
|
args.output.write_text(rendered + "\n", encoding="utf-8")
|
|
else:
|
|
print(rendered)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|