| """Canonical reference table: (make, model, year) -> body_type, segment, avg cost. |
| |
| Built from the cost-bearing datasets (iaai + ganeshsura) during Phase 1 EDA. |
| Persisted as Parquet (with CSV mirror for inspection) at: |
| |
| data/processed/reference_table.parquet |
| data/processed/reference_table.csv |
| |
| The table supports a graceful-degradation `nearest()` lookup used by the |
| fallback estimator and the Tier-2 cost path. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| from dataclasses import dataclass |
| from pathlib import Path |
| from typing import Iterable, Optional |
|
|
| DEFAULT_PATH = Path("data/processed/reference_table.parquet") |
|
|
|
|
| @dataclass |
| class ReferenceRow: |
| make: Optional[str] |
| model: Optional[str] |
| year: Optional[int] |
| body_type: str |
| segment: str |
| avg_cost_usd: float |
| n_samples: int |
| datasets: str |
|
|
| def to_dict(self) -> dict: |
| return self.__dict__.copy() |
|
|
|
|
| |
|
|
|
|
| def build( |
| rows: Iterable[dict], |
| out_path: Path = DEFAULT_PATH, |
| ) -> Path: |
| """Build reference table from per-record dicts. |
| |
| `rows` must contain (at minimum) `make`, `model`, `year`, `body_type`, |
| `segment`, `cost_usd`, `dataset`. Missing fields are normalized to None. |
| """ |
| import pandas as pd |
|
|
| df = pd.DataFrame(list(rows)) |
| if df.empty: |
| out_path.parent.mkdir(parents=True, exist_ok=True) |
| empty = pd.DataFrame( |
| columns=["make", "model", "year", "body_type", "segment", |
| "avg_cost_usd", "n_samples", "datasets"] |
| ) |
| empty.to_parquet(out_path, index=False) |
| empty.to_csv(out_path.with_suffix(".csv"), index=False) |
| return out_path |
|
|
| for col in ("make", "model", "body_type", "segment"): |
| if col in df: |
| df[col] = df[col].fillna("unknown").astype(str).str.lower() |
| else: |
| df[col] = "unknown" |
| if "year" not in df: |
| df["year"] = None |
|
|
| grouped = ( |
| df.groupby(["make", "model", "year", "body_type", "segment"], dropna=False) |
| .agg( |
| avg_cost_usd=("cost_usd", "mean"), |
| n_samples=("cost_usd", "size"), |
| datasets=("dataset", lambda s: ",".join(sorted(set(map(str, s))))), |
| ) |
| .reset_index() |
| ) |
| grouped = grouped.sort_values(["make", "model", "year"]).reset_index(drop=True) |
|
|
| out_path.parent.mkdir(parents=True, exist_ok=True) |
| grouped.to_parquet(out_path, index=False) |
| grouped.to_csv(out_path.with_suffix(".csv"), index=False) |
| return out_path |
|
|
|
|
| def load(path: Path | None = None): |
| import pandas as pd |
| if path is None: |
| path = DEFAULT_PATH |
| if not path.exists(): |
| raise FileNotFoundError( |
| f"Reference table not built yet at {path}. Run Phase 1 EDA notebook " |
| f"or `ccdp data build-reference-table`." |
| ) |
| return pd.read_parquet(path) |
|
|
|
|
| |
|
|
|
|
| def nearest( |
| make: Optional[str] = None, |
| model: Optional[str] = None, |
| year: Optional[int] = None, |
| body_type: Optional[str] = None, |
| segment: Optional[str] = None, |
| path: Path | None = None, |
| ) -> Optional[dict]: |
| """Graceful-degradation lookup. Returns the matched aggregate row + 'how'. |
| |
| Chain: |
| 1. exact (make, model, year) |
| 2. (make, model) any year — most recent first |
| 3. (segment, body_type) |
| 4. (body_type,) only |
| 5. (segment,) only |
| """ |
| df = load(path) |
| if df.empty: |
| return None |
|
|
| def _result(sub, how: str) -> Optional[dict]: |
| if sub.empty: |
| return None |
| |
| if "n_samples" in sub and sub["n_samples"].sum() > 0: |
| cost = (sub["avg_cost_usd"] * sub["n_samples"]).sum() / sub["n_samples"].sum() |
| else: |
| cost = sub["avg_cost_usd"].mean() |
| return { |
| "match_how": how, |
| "n_samples": int(sub["n_samples"].sum()) if "n_samples" in sub else len(sub), |
| "avg_cost_usd": float(cost), |
| "body_type": _mode(sub.get("body_type")), |
| "segment": _mode(sub.get("segment")), |
| "example_model": _example_model(sub), |
| } |
|
|
| if make and model and year is not None: |
| sub = df[(df["make"] == make.lower()) & (df["model"].astype(str).str.startswith(model.lower())) & (df["year"] == year)] |
| r = _result(sub, "exact") |
| if r: |
| return r |
|
|
| if make and model: |
| sub = df[(df["make"] == make.lower()) & (df["model"].astype(str).str.startswith(model.lower()))] |
| r = _result(sub, "make_model_any_year") |
| if r: |
| return r |
|
|
| if segment and body_type: |
| sub = df[(df["segment"] == segment.lower()) & (df["body_type"] == body_type.lower())] |
| r = _result(sub, "segment_body_type") |
| if r: |
| return r |
|
|
| if body_type: |
| sub = df[df["body_type"] == body_type.lower()] |
| r = _result(sub, "body_type") |
| if r: |
| return r |
|
|
| if segment: |
| sub = df[df["segment"] == segment.lower()] |
| r = _result(sub, "segment") |
| if r: |
| return r |
|
|
| return None |
|
|
|
|
| def _mode(series) -> str: |
| if series is None or len(series) == 0: |
| return "unknown" |
| m = series.mode() |
| return str(m.iloc[0]) if not m.empty else "unknown" |
|
|
|
|
| def _example_model(sub) -> str: |
| if sub.empty: |
| return "unknown" |
| top = sub.sort_values("n_samples", ascending=False).iloc[0] |
| parts = [str(top.get("make", "")), str(top.get("model", ""))] |
| return " ".join(p for p in parts if p and p != "unknown").strip() or "unknown" |
|
|
|
|
| |
|
|
|
|
| def coverage_report(path: Path | None = None) -> dict: |
| """Quick health summary for the report generator.""" |
| df = load(path) |
| if df.empty: |
| return {"rows": 0} |
| return { |
| "rows": len(df), |
| "unique_makes": int(df["make"].nunique()), |
| "unique_models": int(df["model"].nunique()), |
| "year_range": [int(df["year"].dropna().min()) if df["year"].notna().any() else None, |
| int(df["year"].dropna().max()) if df["year"].notna().any() else None], |
| "body_type_counts": json.loads(df["body_type"].value_counts().to_json()), |
| "segment_counts": json.loads(df["segment"].value_counts().to_json()), |
| "total_samples": int(df["n_samples"].sum()), |
| "median_avg_cost_usd": float(df["avg_cost_usd"].median()), |
| } |
|
|