Spaces:
Sleeping
Sleeping
| """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() | |