Spaces:
Sleeping
Sleeping
File size: 9,804 Bytes
fef29bc | 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 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | """Demonstrate the CV-to-ML integration: damage_score effect on final CHF price.
This script explicitly quantifies how the Computer Vision damage score drives
the final price recommendation across multiple vehicle types and price segments.
It produces tables and a figure showing the transparent damage-to-price pipeline.
No external API calls are made. Only the deterministic damage_score formula
and CHF calibration are used.
Usage:
python scripts/run_damage_sensitivity.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.append(str(PROJECT_ROOT))
from app.damage_model import (
DAMAGE_WEIGHTS,
MAX_DAMAGE_SCORE,
calculate_adjusted_price,
calculate_damage_score,
)
from app.utils import EUR_TO_CHF_RATE, SWISS_MARKET_FACTOR, eur_to_chf, format_chf
REPORT_PATH = PROJECT_ROOT / "reports/damage_sensitivity_report.md"
FIGURE_PATH = PROJECT_ROOT / "reports/damage_sensitivity_curve.png"
REPRESENTATIVE_VEHICLES = [
{"label": "VW Golf (mid-range)", "base_eur": 15_000},
{"label": "BMW 3 Series (premium)", "base_eur": 28_000},
{"label": "Tesla Model 3 (electric)", "base_eur": 34_000},
{"label": "Toyota Corolla (hybrid)", "base_eur": 20_000},
{"label": "Porsche 911 (luxury)", "base_eur": 118_000},
{"label": "Ford Transit (commercial)", "base_eur": 9_200},
]
DAMAGE_SCORE_STEPS = [0.00, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35]
DAMAGE_LABEL_EXAMPLES = [
("No damage", []),
("Minor scratch", ["scratch"]),
("Scratch + dent", ["scratch", "dent"]),
("Dent + crack", ["dent", "crack"]),
("Lamp broken + glass shatter", ["lamp broken", "glass shatter"]),
("Crushed + lamp broken + crack", ["crushed", "lamp broken", "crack"]),
("Multiple severe (capped at 0.35)", ["crushed", "glass shatter", "lamp broken", "crack", "dent"]),
]
def damage_labels_to_score(labels: list[str]) -> float:
from app.damage_model import DetectedDamage, normalize_damage_label
detections = [
DetectedDamage(label=normalize_damage_label(lbl), confidence=1.0)
for lbl in labels
if DAMAGE_WEIGHTS.get(normalize_damage_label(lbl), 0.05) > 0
]
return calculate_damage_score(detections)
def build_per_vehicle_sensitivity_table() -> pd.DataFrame:
"""For each representative vehicle, show CHF price at each damage score level."""
rows = []
for vehicle in REPRESENTATIVE_VEHICLES:
base_eur = vehicle["base_eur"]
base_chf = eur_to_chf(base_eur) or base_eur
for score in DAMAGE_SCORE_STEPS:
discount_eur, adjusted_eur = calculate_adjusted_price(base_eur, score)
adjusted_chf = eur_to_chf(adjusted_eur) or adjusted_eur
discount_chf = base_chf - adjusted_chf
rows.append({
"vehicle": vehicle["label"],
"base_chf": round(base_chf),
"damage_score": score,
"discount_chf": round(discount_chf),
"adjusted_chf": round(adjusted_chf),
"price_reduction_pct": round(score * 100, 1),
})
return pd.DataFrame(rows)
def build_label_to_score_table() -> pd.DataFrame:
"""Show how CV-detected damage labels map to damage scores and CHF discounts."""
rows = []
for description, labels in DAMAGE_LABEL_EXAMPLES:
score = damage_labels_to_score(labels)
for vehicle in REPRESENTATIVE_VEHICLES[:3]:
base_eur = vehicle["base_eur"]
discount_eur, adjusted_eur = calculate_adjusted_price(base_eur, score)
adjusted_chf = eur_to_chf(adjusted_eur) or adjusted_eur
base_chf = eur_to_chf(base_eur) or base_eur
discount_chf = base_chf - adjusted_chf
rows.append({
"damage_description": description,
"damage_labels": ", ".join(labels) or "none",
"damage_score": score,
"vehicle": vehicle["label"],
"base_chf": round(base_chf),
"discount_chf": round(discount_chf),
"adjusted_chf": round(adjusted_chf),
})
return pd.DataFrame(rows)
def save_sensitivity_figure(df: pd.DataFrame) -> None:
try:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 5))
for vehicle in REPRESENTATIVE_VEHICLES:
subset = df[df["vehicle"] == vehicle["label"]]
ax.plot(
subset["damage_score"],
subset["adjusted_chf"],
marker="o",
label=vehicle["label"],
)
ax.set_xlabel("Damage Score (0 = no damage, 0.35 = max)")
ax.set_ylabel("Adjusted Listing Price (CHF)")
ax.set_title("CV Damage Score β Final CHF Listing Price")
ax.legend(fontsize=8, loc="upper right")
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig(FIGURE_PATH, dpi=120)
plt.close(fig)
print(f"Saved figure: {FIGURE_PATH}")
except ImportError:
pass
def write_report(
per_vehicle_df: pd.DataFrame,
label_score_df: pd.DataFrame,
) -> None:
REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
lines = [
"# CV-to-ML Integration: Damage Sensitivity Analysis",
"",
"This report explicitly demonstrates how the Computer Vision block drives",
"the final CHF price recommendation in the integrated pipeline.",
"",
"## Integration Pipeline",
"",
"```",
"Vehicle photos",
" β OpenAI Vision / local CV model",
" β damage labels (e.g. 'scratch', 'dent', 'lamp broken')",
" β damage_score = Ξ£ weight_i Γ confidence_i (capped at 0.35)",
" β CHF discount = base_CHF Γ damage_score",
" β adjusted_CHF = base_CHF β CHF discount",
"```",
"",
f"Calibration constants: EUR_TO_CHF_RATE = {EUR_TO_CHF_RATE}, "
f"SWISS_MARKET_FACTOR = {SWISS_MARKET_FACTOR}",
"",
"## Damage Label Weights",
"",
"| Damage label | Weight | Effect at CHF 20,000 base |",
"|---|---:|---:|",
]
for label, weight in sorted(DAMAGE_WEIGHTS.items(), key=lambda x: -x[1]):
if weight > 0:
sample_base_chf = eur_to_chf(20_000) or 20_000
effect_chf = round(sample_base_chf * weight)
lines.append(f"| {label} | {weight:.2f} | βCHF {effect_chf:,} |")
lines.extend([
"",
f"Maximum damage score cap: **{MAX_DAMAGE_SCORE}** (prevents unrealistic price collapse)",
"",
"## Damage Labels β Score β Price: Reference Table",
"",
"| Damage description | Labels | Score | VW Golf base CHF | Discount CHF | Adjusted CHF |",
"|---|---|---:|---:|---:|---:|",
])
vw_rows = label_score_df[label_score_df["vehicle"].str.contains("VW Golf")]
for _, row in vw_rows.iterrows():
lines.append(
f"| {row['damage_description']} | {row['damage_labels']} | "
f"{row['damage_score']:.3f} | {row['base_chf']:,} | "
f"{row['discount_chf']:,} | {row['adjusted_chf']:,} |"
)
lines.extend([
"",
"## Price Sensitivity Across Vehicle Types",
"",
"| Vehicle | Base CHF | Damage 0% | Damage 5% | Damage 10% | Damage 20% | Damage 35% (max) |",
"|---|---:|---:|---:|---:|---:|---:|",
])
for vehicle in REPRESENTATIVE_VEHICLES:
subset = per_vehicle_df[per_vehicle_df["vehicle"] == vehicle["label"]]
vals = {
row["damage_score"]: row["adjusted_chf"]
for _, row in subset.iterrows()
}
base_chf = subset.iloc[0]["base_chf"]
lines.append(
f"| {vehicle['label']} | {base_chf:,} | "
f"{vals.get(0.00, 'β'):,} | {vals.get(0.05, 'β'):,} | "
f"{vals.get(0.10, 'β'):,} | {vals.get(0.20, 'β'):,} | "
f"{vals.get(0.35, 'β'):,} |"
)
lines.extend([
"",
f"Figure: `{FIGURE_PATH.relative_to(PROJECT_ROOT)}`",
"",
"## Key Findings",
"",
"- Damage score = 0.00 (no CV damage detected): no price discount applied",
f"- Damage score = 0.05 (minor scratch): ~5% price reduction",
f"- Damage score = 0.20 (moderate damage): ~20% price reduction",
f"- Damage score = 0.35 (severe/multiple, cap): ~35% price reduction",
"- The cap at 0.35 prevents a full-price collapse for multiple simultaneous detections",
"- Price reduction is proportional in CHF, so absolute discounts are higher for expensive vehicles",
"",
"## Integration Evidence",
"",
"The damage_score produced by the CV block directly changes the NLP block inputs",
"and the final CHF listing price recommendation. This is the primary integration",
"channel between Computer Vision and ML Numeric Data / NLP in this application.",
"The scoring formula is fully transparent and deterministic, allowing users to",
"understand exactly how a detected 'dent' or 'lamp broken' reduces their asking price.",
])
REPORT_PATH.write_text("\n".join(lines), encoding="utf-8")
def main() -> None:
print("Computing damage sensitivity tables...")
per_vehicle_df = build_per_vehicle_sensitivity_table()
label_score_df = build_label_to_score_table()
save_sensitivity_figure(per_vehicle_df)
write_report(per_vehicle_df, label_score_df)
print(f"Wrote {REPORT_PATH}")
if __name__ == "__main__":
main()
|