Spaces:
Runtime error
Runtime error
File size: 6,658 Bytes
4ce7a7e | 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 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | """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()),
}
|