theDocWho's picture
Initial commit: end-to-end car damage + repair-cost predictor
4ce7a7e
Raw
History Blame Contribute Delete
6.66 kB
"""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 # comma-joined list of source dataset names
def to_dict(self) -> dict:
return self.__dict__.copy()
# --- build --------------------------------------------------------------
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 # local import — pandas is in [ml] extras
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)
# --- lookup -------------------------------------------------------------
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
# weighted average by sample count
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"
# --- coverage report ----------------------------------------------------
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()),
}