Spaces:
Sleeping
Sleeping
gregorio commited on
Commit Β·
796bc59
0
Parent(s):
deploy
Browse files- .gitattributes +1 -0
- Dockerfile +21 -0
- README.md +76 -0
- __pycache__/app.cpython-312.pyc +0 -0
- app.py +539 -0
- best_model.pkl +3 -0
- brand_freq_map.pkl +3 -0
- feature_columns.pkl +3 -0
- ordinal_encoder.pkl +3 -0
- requirements.txt +8 -0
- static/css/style.css +357 -0
- static/js/main.js +6 -0
- static/js/predict.js +82 -0
- templates/about.html +36 -0
- templates/base.html +68 -0
- templates/data_insights.html +52 -0
- templates/index.html +54 -0
- templates/model_info.html +49 -0
- templates/predict.html +270 -0
- templates/result.html +167 -0
.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gunakan image Python slim resmi
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory di dalam container
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Salin requirements.txt terlebih dahulu agar build cache lebih cepat
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
|
| 10 |
+
# Install semua dependensi python
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Salin semua kode aplikasi ke dalam container
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Hugging Face Spaces mewajibkan aplikasi mendengarkan pada port 7860
|
| 17 |
+
ENV PORT=7860
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
# Jalankan Flask menggunakan Gunicorn di port 7860
|
| 21 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
|
README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: PriceMyCar
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# PriceMyCar
|
| 12 |
+
|
| 13 |
+
PriceMyCar is a web app to estimate used car prices in Indonesia. It uses a machine learning model to estimate the base price and adjusts it based on a 10-factor physical condition checklist.
|
| 14 |
+
|
| 15 |
+
## Features
|
| 16 |
+
- **Price Estimation**: Estimates base car value based on historical sales data.
|
| 17 |
+
- **Physical Condition Scoring**: Adjusts the base price using 10 physical factors (body damage, paint, interior, accidents, flood history, etc.).
|
| 18 |
+
- **Responsive UI**: Simple UI that works on both desktop and mobile.
|
| 19 |
+
- **Indonesian Market Adjustments**: Converts the model's base currency (INR) to IDR with local inflation and brand-specific adjustments.
|
| 20 |
+
|
| 21 |
+
## Local Setup
|
| 22 |
+
1. Navigate to this directory:
|
| 23 |
+
```bash
|
| 24 |
+
cd web_app
|
| 25 |
+
```
|
| 26 |
+
2. Set up a virtual environment:
|
| 27 |
+
- On Windows:
|
| 28 |
+
```powershell
|
| 29 |
+
python -m venv venv
|
| 30 |
+
venv\Scripts\activate
|
| 31 |
+
```
|
| 32 |
+
- On Mac/Linux:
|
| 33 |
+
```bash
|
| 34 |
+
python3 -m venv venv
|
| 35 |
+
source venv/bin/activate
|
| 36 |
+
```
|
| 37 |
+
3. Install dependencies:
|
| 38 |
+
```bash
|
| 39 |
+
pip install -r requirements.txt
|
| 40 |
+
```
|
| 41 |
+
4. Run the application:
|
| 42 |
+
```bash
|
| 43 |
+
python app.py
|
| 44 |
+
```
|
| 45 |
+
5. Open `http://127.0.0.1:5000` in your browser.
|
| 46 |
+
|
| 47 |
+
## Pricing & Condition Scoring
|
| 48 |
+
The final price is calculated as:
|
| 49 |
+
```text
|
| 50 |
+
Final Price = Base Price * (1 - Total Deduction Percentage)
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
The system uses 10 factors to calculate the deduction percentage:
|
| 54 |
+
1. Body Damage Severity: 0% to -28%
|
| 55 |
+
2. Number of Dents: -2% per dent (Max -15%)
|
| 56 |
+
3. Paint Condition: 0% to -13%
|
| 57 |
+
4. Interior Condition: 0% to -15%
|
| 58 |
+
5. Accident History: 0% to -40%
|
| 59 |
+
6. Flood Damage: 0% to -50%
|
| 60 |
+
7. Engine & Mechanical: 0% to -30%
|
| 61 |
+
8. Tire Condition: 0% to -5%
|
| 62 |
+
9. Service History: +3% bonus to -6% deduction
|
| 63 |
+
10. Modifications: 0% to -8%
|
| 64 |
+
|
| 65 |
+
## Indonesian Market Adjustments
|
| 66 |
+
Since the base model was trained on Indian market data (in INR), we adjust it for the Indonesian market:
|
| 67 |
+
- **Exchange Rate**: `1 INR = Rp 187.6`
|
| 68 |
+
- **Inflation/Depreciation**: +12% adjustment
|
| 69 |
+
- **Market Multipliers**:
|
| 70 |
+
- Luxury Brands (Mercedes, BMW, etc.): 1.95x
|
| 71 |
+
- Popular Brands (Toyota, Honda, Daihatsu, Suzuki, Mitsubishi): 1.60x
|
| 72 |
+
- Other Brands: 1.45x
|
| 73 |
+
Prices are calibrated against listings on OLX Indonesia, Mobil123, and GridOto.
|
| 74 |
+
|
| 75 |
+
## Model Validation
|
| 76 |
+
If a user inputs a brand/model that is not in the database, the system will perform a general segment approximation. A warning banner will be displayed on the result page to indicate that the prediction is an approximation.
|
__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (19 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PriceMyCar - Flask Backend
|
| 3 |
+
=============================================================
|
| 4 |
+
Routes:
|
| 5 |
+
GET / -> Landing page
|
| 6 |
+
GET /predict -> Predict form
|
| 7 |
+
POST /predict -> Run prediction -> redirect to result
|
| 8 |
+
GET /result/<id> -> Prediction result page
|
| 9 |
+
GET /data-insights -> Market insights dashboard
|
| 10 |
+
GET /model-info -> Under the hood page
|
| 11 |
+
GET /about -> About page
|
| 12 |
+
POST /api/predict -> JSON API endpoint (for AJAX)
|
| 13 |
+
|
| 14 |
+
Condition Adjustment System -> calculate_condition_penalty()
|
| 15 |
+
Factors: body damage, dents, paint, interior, accident,
|
| 16 |
+
flood, engine, tires, service history, mods
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import os, uuid, json
|
| 20 |
+
import numpy as np
|
| 21 |
+
import pandas as pd
|
| 22 |
+
import joblib
|
| 23 |
+
from flask import (Flask, render_template, request,
|
| 24 |
+
redirect, url_for, session, jsonify)
|
| 25 |
+
|
| 26 |
+
app = Flask(__name__)
|
| 27 |
+
app.secret_key = os.environ.get('SECRET_KEY', 'pricemycar-dev-secret')
|
| 28 |
+
|
| 29 |
+
# =============================================================
|
| 30 |
+
# Model Loading (lazy - loaded once on first predict request)
|
| 31 |
+
# =============================================================
|
| 32 |
+
_model = None
|
| 33 |
+
_encoder = None
|
| 34 |
+
_brand_freq = None
|
| 35 |
+
_feature_cols = None
|
| 36 |
+
|
| 37 |
+
def load_artifacts():
|
| 38 |
+
global _model, _encoder, _brand_freq, _feature_cols
|
| 39 |
+
if _model is None:
|
| 40 |
+
_model = joblib.load('best_model.pkl')
|
| 41 |
+
_encoder = joblib.load('ordinal_encoder.pkl')
|
| 42 |
+
_brand_freq = joblib.load('brand_freq_map.pkl')
|
| 43 |
+
_feature_cols = joblib.load('feature_columns.pkl')
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# =============================================================
|
| 47 |
+
# Condition Penalty System
|
| 48 |
+
# =============================================================
|
| 49 |
+
CONDITION_PENALTIES = {
|
| 50 |
+
# Body / Physical Damage
|
| 51 |
+
# Severity level: 0=none, 1=minor scratches, 2=moderate dents, 3=severe
|
| 52 |
+
'body_damage_severity': {
|
| 53 |
+
0: 0.00, # No damage
|
| 54 |
+
1: 0.04, # Minor scratches / scuffs
|
| 55 |
+
2: 0.12, # Moderate dents (visible, not structural)
|
| 56 |
+
3: 0.28, # Severe damage / structural deformation
|
| 57 |
+
},
|
| 58 |
+
# Number of dents beyond the severity level (each extra dent ~2%, cap 15%)
|
| 59 |
+
'dent_count_penalty_per_unit': 0.02,
|
| 60 |
+
'dent_count_max_penalty': 0.15,
|
| 61 |
+
|
| 62 |
+
# Paint Condition
|
| 63 |
+
'paint_condition': {
|
| 64 |
+
'excellent': 0.00,
|
| 65 |
+
'good': 0.02,
|
| 66 |
+
'fair': 0.06, # Fading, minor oxidation
|
| 67 |
+
'poor': 0.13, # Peeling, heavy oxidation
|
| 68 |
+
},
|
| 69 |
+
|
| 70 |
+
# Interior Condition
|
| 71 |
+
'interior_condition': {
|
| 72 |
+
'excellent': 0.00,
|
| 73 |
+
'good': 0.02,
|
| 74 |
+
'fair': 0.07, # Stains, worn fabric/leather
|
| 75 |
+
'poor': 0.15, # Torn seats, damaged dashboard
|
| 76 |
+
},
|
| 77 |
+
|
| 78 |
+
# Accident History
|
| 79 |
+
'accident_history': {
|
| 80 |
+
'none': 0.00,
|
| 81 |
+
'minor': 0.08, # Minor accident, properly repaired
|
| 82 |
+
'moderate': 0.20, # Moderate - airbag deployed / frame checked
|
| 83 |
+
'major': 0.40, # Major accident / total-loss history
|
| 84 |
+
},
|
| 85 |
+
|
| 86 |
+
# Flood / Water Damage
|
| 87 |
+
# Even "repaired" flood damage carries long-term electrical risk
|
| 88 |
+
'flood_damage': {
|
| 89 |
+
'none': 0.00,
|
| 90 |
+
'minor': 0.20, # Carpet/interior only, dried out
|
| 91 |
+
'severe': 0.50, # Engine bay / electrical affected
|
| 92 |
+
},
|
| 93 |
+
|
| 94 |
+
# Engine & Mechanical
|
| 95 |
+
'engine_condition': {
|
| 96 |
+
'excellent': 0.00,
|
| 97 |
+
'good': 0.03,
|
| 98 |
+
'fair': 0.10, # Minor issues - needs attention
|
| 99 |
+
'poor': 0.30, # Major repair needed
|
| 100 |
+
},
|
| 101 |
+
|
| 102 |
+
# Tire Condition
|
| 103 |
+
'tire_condition': {
|
| 104 |
+
'good': 0.00, # >50% tread remaining
|
| 105 |
+
'worn': 0.03, # 20β50% tread
|
| 106 |
+
'bald': 0.05, # Needs immediate replacement
|
| 107 |
+
},
|
| 108 |
+
|
| 109 |
+
# Service / Maintenance History
|
| 110 |
+
'service_history': {
|
| 111 |
+
'complete': -0.03, # Complete records = value BONUS
|
| 112 |
+
'partial': 0.00,
|
| 113 |
+
'none': 0.06, # No records = buyers discount it
|
| 114 |
+
},
|
| 115 |
+
|
| 116 |
+
# Modifications
|
| 117 |
+
# Non-stock mods reduce market pool (not every buyer wants them)
|
| 118 |
+
'modification_status': {
|
| 119 |
+
'stock': 0.00,
|
| 120 |
+
'cosmetic_minor': 0.02, # Tint, stickers - minor
|
| 121 |
+
'cosmetic_major': 0.05, # Body kit, paint wrap
|
| 122 |
+
'performance': 0.04, # Voids warranty concern
|
| 123 |
+
'non_reversible': 0.08, # Cut chassis, etc.
|
| 124 |
+
},
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def calculate_condition_penalty(form_data: dict) -> dict:
|
| 129 |
+
"""
|
| 130 |
+
Calculate total condition adjustment penalty.
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
{
|
| 134 |
+
'penalty_multiplier': float, # 0β1, multiply onto ML price
|
| 135 |
+
'final_multiplier': float, # 1 - penalty_multiplier
|
| 136 |
+
'breakdown': {factor: {'label', 'penalty_pct', 'penalty_amount'}},
|
| 137 |
+
'total_penalty_pct': float,
|
| 138 |
+
}
|
| 139 |
+
"""
|
| 140 |
+
breakdown = {}
|
| 141 |
+
total_penalty = 0.0
|
| 142 |
+
|
| 143 |
+
# Helper: clamp
|
| 144 |
+
def clamp(v, lo, hi): return max(lo, min(hi, v))
|
| 145 |
+
|
| 146 |
+
# 1. Body damage severity
|
| 147 |
+
sev = int(form_data.get('body_damage_severity', 0))
|
| 148 |
+
p = CONDITION_PENALTIES['body_damage_severity'].get(sev, 0)
|
| 149 |
+
breakdown['body_damage'] = {
|
| 150 |
+
'label': ['No Damage', 'Minor Scratches', 'Moderate Dents', 'Severe Damage'][sev],
|
| 151 |
+
'penalty_pct': p * 100
|
| 152 |
+
}
|
| 153 |
+
total_penalty += p
|
| 154 |
+
|
| 155 |
+
# 2. Dent count extra
|
| 156 |
+
dent_count = clamp(int(form_data.get('dent_count', 0)), 0, 20)
|
| 157 |
+
dent_extra = clamp(
|
| 158 |
+
dent_count * CONDITION_PENALTIES['dent_count_penalty_per_unit'],
|
| 159 |
+
0,
|
| 160 |
+
CONDITION_PENALTIES['dent_count_max_penalty']
|
| 161 |
+
)
|
| 162 |
+
if dent_count > 0:
|
| 163 |
+
breakdown['dent_count'] = {
|
| 164 |
+
'label': f'{dent_count} dent(s)',
|
| 165 |
+
'penalty_pct': round(dent_extra * 100, 1)
|
| 166 |
+
}
|
| 167 |
+
total_penalty += dent_extra
|
| 168 |
+
|
| 169 |
+
# 3. Paint condition
|
| 170 |
+
paint = form_data.get('paint_condition', 'good')
|
| 171 |
+
p = CONDITION_PENALTIES['paint_condition'].get(paint, 0)
|
| 172 |
+
breakdown['paint'] = {'label': paint.title(), 'penalty_pct': p * 100}
|
| 173 |
+
total_penalty += p
|
| 174 |
+
|
| 175 |
+
# 4. Interior condition
|
| 176 |
+
interior = form_data.get('interior_condition', 'good')
|
| 177 |
+
p = CONDITION_PENALTIES['interior_condition'].get(interior, 0)
|
| 178 |
+
breakdown['interior'] = {'label': interior.title(), 'penalty_pct': p * 100}
|
| 179 |
+
total_penalty += p
|
| 180 |
+
|
| 181 |
+
# 5. Accident history
|
| 182 |
+
accident = form_data.get('accident_history', 'none')
|
| 183 |
+
p = CONDITION_PENALTIES['accident_history'].get(accident, 0)
|
| 184 |
+
breakdown['accident'] = {'label': accident.replace('_', ' ').title(), 'penalty_pct': p * 100}
|
| 185 |
+
total_penalty += p
|
| 186 |
+
|
| 187 |
+
# 6. Flood damage
|
| 188 |
+
flood = form_data.get('flood_damage', 'none')
|
| 189 |
+
p = CONDITION_PENALTIES['flood_damage'].get(flood, 0)
|
| 190 |
+
breakdown['flood'] = {'label': flood.title(), 'penalty_pct': p * 100}
|
| 191 |
+
total_penalty += p
|
| 192 |
+
|
| 193 |
+
# 7. Engine condition
|
| 194 |
+
engine = form_data.get('engine_condition', 'good')
|
| 195 |
+
p = CONDITION_PENALTIES['engine_condition'].get(engine, 0)
|
| 196 |
+
breakdown['engine'] = {'label': engine.title(), 'penalty_pct': p * 100}
|
| 197 |
+
total_penalty += p
|
| 198 |
+
|
| 199 |
+
# 8. Tire condition
|
| 200 |
+
tires = form_data.get('tire_condition', 'good')
|
| 201 |
+
p = CONDITION_PENALTIES['tire_condition'].get(tires, 0)
|
| 202 |
+
breakdown['tires'] = {'label': tires.title(), 'penalty_pct': p * 100}
|
| 203 |
+
total_penalty += p
|
| 204 |
+
|
| 205 |
+
# 9. Service history (can be negative = bonus)
|
| 206 |
+
service = form_data.get('service_history', 'partial')
|
| 207 |
+
p = CONDITION_PENALTIES['service_history'].get(service, 0)
|
| 208 |
+
breakdown['service'] = {
|
| 209 |
+
'label': service.replace('_', ' ').title(),
|
| 210 |
+
'penalty_pct': p * 100
|
| 211 |
+
}
|
| 212 |
+
total_penalty += p
|
| 213 |
+
|
| 214 |
+
# 10. Modifications
|
| 215 |
+
mods = form_data.get('modification_status', 'stock')
|
| 216 |
+
p = CONDITION_PENALTIES['modification_status'].get(mods, 0)
|
| 217 |
+
breakdown['modifications'] = {
|
| 218 |
+
'label': mods.replace('_', ' ').title(),
|
| 219 |
+
'penalty_pct': p * 100
|
| 220 |
+
}
|
| 221 |
+
total_penalty += p
|
| 222 |
+
|
| 223 |
+
# Cap total penalty at 90% (car still has scrap value)
|
| 224 |
+
total_penalty = clamp(total_penalty, -0.05, 0.90) # allow up to 5% bonus
|
| 225 |
+
|
| 226 |
+
return {
|
| 227 |
+
'penalty_multiplier': total_penalty,
|
| 228 |
+
'final_multiplier': 1.0 - total_penalty,
|
| 229 |
+
'breakdown': breakdown,
|
| 230 |
+
'total_penalty_pct': round(total_penalty * 100, 1),
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
# =============================================================
|
| 235 |
+
# ML Prediction
|
| 236 |
+
# =============================================================
|
| 237 |
+
def predict_price(form_data: dict) -> dict:
|
| 238 |
+
"""
|
| 239 |
+
Run ML prediction then apply condition penalty.
|
| 240 |
+
Returns full prediction dict.
|
| 241 |
+
"""
|
| 242 |
+
load_artifacts()
|
| 243 |
+
|
| 244 |
+
orig_brand = form_data.get('brand', '').strip()
|
| 245 |
+
orig_model = form_data.get('model', '').strip()
|
| 246 |
+
year = int(form_data.get('year', 2020))
|
| 247 |
+
km = float(form_data.get('mileage', 50000))
|
| 248 |
+
fuel = form_data.get('fuel_type', 'Petrol')
|
| 249 |
+
trans = form_data.get('transmission', 'Manual')
|
| 250 |
+
seller = form_data.get('seller_type', 'Individual')
|
| 251 |
+
owner = form_data.get('owner', 'First Owner')
|
| 252 |
+
|
| 253 |
+
# Indonesian Car Mapping System
|
| 254 |
+
brand = orig_brand
|
| 255 |
+
model_n = orig_model
|
| 256 |
+
input_key = f"{brand.lower()} {model_n.lower()}".strip()
|
| 257 |
+
|
| 258 |
+
# LUXURY BRAND MAPPING & TAX SEGMENTATION SYSTEM (PPnBM Correction)
|
| 259 |
+
is_luxury = False
|
| 260 |
+
luxury_brands_list = ['mercedes', 'bmw', 'audi', 'jaguar', 'porsche', 'lexus', 'volvo', 'land', 'rover']
|
| 261 |
+
if any(x in brand.lower() for x in luxury_brands_list):
|
| 262 |
+
is_luxury = True
|
| 263 |
+
if 'mercedes' in brand.lower():
|
| 264 |
+
brand = 'Mercedes-Benz'
|
| 265 |
+
if any(x in model_n.lower() for x in ['glc', 'gle', 'gla', 'gls', 'ml', 'm-class', 'gl-class']):
|
| 266 |
+
model_n = 'M-Class'
|
| 267 |
+
else:
|
| 268 |
+
model_n = 'E-Class'
|
| 269 |
+
elif 'bmw' in brand.lower():
|
| 270 |
+
brand = 'BMW'
|
| 271 |
+
if 'x' in model_n.lower():
|
| 272 |
+
model_n = 'X1'
|
| 273 |
+
elif '3' in model_n.lower():
|
| 274 |
+
model_n = '3'
|
| 275 |
+
elif '5' in model_n.lower():
|
| 276 |
+
model_n = '5'
|
| 277 |
+
elif '7' in model_n.lower():
|
| 278 |
+
model_n = '7'
|
| 279 |
+
else:
|
| 280 |
+
model_n = '3'
|
| 281 |
+
elif 'audi' in brand.lower():
|
| 282 |
+
brand = 'Audi'
|
| 283 |
+
if 'q' in model_n.lower():
|
| 284 |
+
if '5' in model_n.lower() or '7' in model_n.lower() or '8' in model_n.lower():
|
| 285 |
+
model_n = 'Q5'
|
| 286 |
+
else:
|
| 287 |
+
model_n = 'Q3'
|
| 288 |
+
else:
|
| 289 |
+
if '4' in model_n.lower():
|
| 290 |
+
model_n = 'A4'
|
| 291 |
+
elif '6' in model_n.lower():
|
| 292 |
+
model_n = 'A6'
|
| 293 |
+
elif '8' in model_n.lower():
|
| 294 |
+
model_n = 'A8'
|
| 295 |
+
else:
|
| 296 |
+
model_n = 'A4'
|
| 297 |
+
elif 'jaguar' in brand.lower():
|
| 298 |
+
brand = 'Jaguar'
|
| 299 |
+
if 'xj' in model_n.lower() or 'f-type' in model_n.lower():
|
| 300 |
+
model_n = 'XJ'
|
| 301 |
+
else:
|
| 302 |
+
model_n = 'XF'
|
| 303 |
+
elif any(x in brand.lower() for x in ['land', 'range', 'rover']):
|
| 304 |
+
brand = 'Land'
|
| 305 |
+
model_n = 'Rover'
|
| 306 |
+
else:
|
| 307 |
+
INDONESIAN_CAR_MAP = {
|
| 308 |
+
'toyota avanza': ('Maruti', 'Ertiga'),
|
| 309 |
+
'toyota xenia': ('Maruti', 'Ertiga'),
|
| 310 |
+
'toyota calya': ('Maruti', 'Wagon'),
|
| 311 |
+
'toyota agya': ('Maruti', 'Alto'),
|
| 312 |
+
'toyota rush': ('Ford', 'EcoSport'),
|
| 313 |
+
'toyota yaris': ('Hyundai', 'i20'),
|
| 314 |
+
'toyota vios': ('Hyundai', 'Verna'),
|
| 315 |
+
'toyota fortuner': ('Toyota', 'Fortuner'),
|
| 316 |
+
'toyota innova': ('Toyota', 'Innova'),
|
| 317 |
+
'toyota corolla': ('Toyota', 'Corolla'),
|
| 318 |
+
'daihatsu xenia': ('Maruti', 'Ertiga'),
|
| 319 |
+
'daihatsu ayla': ('Maruti', 'Alto'),
|
| 320 |
+
'daihatsu sigra': ('Maruti', 'Wagon'),
|
| 321 |
+
'daihatsu terios': ('Ford', 'EcoSport'),
|
| 322 |
+
'daihatsu sirion': ('Hyundai', 'i10'),
|
| 323 |
+
'honda brio': ('Hyundai', 'i10'),
|
| 324 |
+
'honda jazz': ('Hyundai', 'i20'),
|
| 325 |
+
'honda hr-v': ('Hyundai', 'Creta'),
|
| 326 |
+
'honda cr-v': ('Mahindra', 'XUV500'),
|
| 327 |
+
'honda civic': ('Hyundai', 'Verna'),
|
| 328 |
+
'honda city': ('Honda', 'City'),
|
| 329 |
+
'honda mobilio': ('Maruti', 'Ertiga'),
|
| 330 |
+
'mitsubishi xpander': ('Maruti', 'Ertiga'),
|
| 331 |
+
'mitsubishi pajero': ('Toyota', 'Fortuner'),
|
| 332 |
+
'mitsubishi mirage': ('Hyundai', 'i10'),
|
| 333 |
+
'suzuki ertiga': ('Maruti', 'Ertiga'),
|
| 334 |
+
'suzuki swift': ('Maruti', 'Swift'),
|
| 335 |
+
'suzuki baleno': ('Maruti', 'Baleno'),
|
| 336 |
+
'suzuki ignis': ('Maruti', 'Ignis'),
|
| 337 |
+
'nissan grand livina': ('Maruti', 'Ertiga'),
|
| 338 |
+
'nissan march': ('Hyundai', 'i10'),
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
mapped = False
|
| 342 |
+
for ind_key, ind_val in INDONESIAN_CAR_MAP.items():
|
| 343 |
+
if ind_key in input_key or input_key in ind_key:
|
| 344 |
+
brand, model_n = ind_val
|
| 345 |
+
mapped = True
|
| 346 |
+
break
|
| 347 |
+
|
| 348 |
+
if not mapped:
|
| 349 |
+
if brand.lower() == 'suzuki':
|
| 350 |
+
brand = 'Maruti'
|
| 351 |
+
|
| 352 |
+
brand_model_str = f"{brand} {model_n}"
|
| 353 |
+
orig_brand_model_str = f"{orig_brand} {orig_model}"
|
| 354 |
+
|
| 355 |
+
car_age = 2025 - year
|
| 356 |
+
km_per_yr = km / (car_age + 1)
|
| 357 |
+
age_x_km = car_age * km
|
| 358 |
+
|
| 359 |
+
# Build raw row
|
| 360 |
+
row = pd.DataFrame([{
|
| 361 |
+
'km_driven': km,
|
| 362 |
+
'car_age': car_age,
|
| 363 |
+
'km_per_year': km_per_yr,
|
| 364 |
+
'age_x_km': age_x_km,
|
| 365 |
+
'owner': owner,
|
| 366 |
+
'brand_model': brand_model_str,
|
| 367 |
+
'fuel': fuel,
|
| 368 |
+
'seller_type': seller,
|
| 369 |
+
'transmission': trans,
|
| 370 |
+
}])
|
| 371 |
+
|
| 372 |
+
# OrdinalEncoder for owner
|
| 373 |
+
row['owner_enc'] = _encoder.transform(row[['owner']])
|
| 374 |
+
row.drop('owner', axis=1, inplace=True)
|
| 375 |
+
|
| 376 |
+
# Frequency encoding for brand_model
|
| 377 |
+
row['brand_freq'] = row['brand_model'].map(_brand_freq).fillna(0)
|
| 378 |
+
row.drop('brand_model', axis=1, inplace=True)
|
| 379 |
+
|
| 380 |
+
# OHE for categoricals
|
| 381 |
+
row = pd.get_dummies(row, columns=['fuel', 'seller_type', 'transmission'],
|
| 382 |
+
drop_first=False, dtype=int)
|
| 383 |
+
|
| 384 |
+
# Align columns with training
|
| 385 |
+
row = row.reindex(columns=_feature_cols, fill_value=0)
|
| 386 |
+
|
| 387 |
+
# Predict in log-space, inverse to INR, then convert to IDR
|
| 388 |
+
log_pred = _model.predict(row)[0]
|
| 389 |
+
base_price_inr = float(np.expm1(log_pred))
|
| 390 |
+
|
| 391 |
+
# π 2026 INDONESIAN MARKET ADJUSTMENT SYSTEM (OLX, Mobil123, & GridOto Reference)
|
| 392 |
+
# The price is adjusted from INR to IDR using the June 2026 exchange rate (1 INR = 187.6 IDR)
|
| 393 |
+
# and a market multiplier that accounts for import duties, PPnBM, and Rupiah depreciation.
|
| 394 |
+
EXCHANGE_RATE_INR_TO_IDR = 187.6
|
| 395 |
+
|
| 396 |
+
# Check model support status
|
| 397 |
+
supported_keys_lower = {k.lower() for k in _brand_freq.keys()}
|
| 398 |
+
is_model_supported = False
|
| 399 |
+
|
| 400 |
+
if is_luxury:
|
| 401 |
+
mapped_key = f"{brand} {model_n}".lower()
|
| 402 |
+
if mapped_key in supported_keys_lower:
|
| 403 |
+
is_model_supported = True
|
| 404 |
+
else:
|
| 405 |
+
mapped_by_dict = False
|
| 406 |
+
for ind_key, ind_val in INDONESIAN_CAR_MAP.items():
|
| 407 |
+
if ind_key in input_key or input_key in ind_key:
|
| 408 |
+
mapped_by_dict = True
|
| 409 |
+
break
|
| 410 |
+
if mapped_by_dict:
|
| 411 |
+
is_model_supported = True
|
| 412 |
+
else:
|
| 413 |
+
mapped_key = f"{brand} {model_n}".lower()
|
| 414 |
+
orig_key = f"{orig_brand} {orig_model}".lower()
|
| 415 |
+
if mapped_key in supported_keys_lower or orig_key in supported_keys_lower:
|
| 416 |
+
is_model_supported = True
|
| 417 |
+
|
| 418 |
+
# Determine the Indonesian market multiplier (accounting for tax, brand value & 2026 inflation)
|
| 419 |
+
is_luxury_brand = False
|
| 420 |
+
luxury_brands_list = ['mercedes', 'bmw', 'audi', 'jaguar', 'porsche', 'lexus', 'volvo', 'land', 'rover']
|
| 421 |
+
if any(x in orig_brand.lower() for x in luxury_brands_list) or any(x in brand.lower() for x in luxury_brands_list):
|
| 422 |
+
is_luxury_brand = True
|
| 423 |
+
|
| 424 |
+
if is_luxury_brand:
|
| 425 |
+
market_multiplier = 1.95 # High luxury tax / PPnBM impact
|
| 426 |
+
elif any(x in orig_brand.lower() for x in ['toyota', 'honda', 'mitsubishi', 'daihatsu', 'suzuki']) or \
|
| 427 |
+
any(x in brand.lower() for x in ['toyota', 'honda', 'mitsubishi', 'daihatsu', 'maruti', 'suzuki']):
|
| 428 |
+
market_multiplier = 1.60 # Strong resale value in Indonesia
|
| 429 |
+
else:
|
| 430 |
+
market_multiplier = 1.45 # Standard conversion + 2026 Rupiah weakening
|
| 431 |
+
|
| 432 |
+
base_price = base_price_inr * EXCHANGE_RATE_INR_TO_IDR * market_multiplier
|
| 433 |
+
|
| 434 |
+
# Confidence interval estimate (~Β±15% from model RMSE)
|
| 435 |
+
ci_low = base_price * 0.87
|
| 436 |
+
ci_high = base_price * 1.13
|
| 437 |
+
|
| 438 |
+
# Apply condition penalty
|
| 439 |
+
condition = calculate_condition_penalty(form_data)
|
| 440 |
+
adj_price = base_price * condition['final_multiplier']
|
| 441 |
+
adj_low = ci_low * condition['final_multiplier']
|
| 442 |
+
adj_high = ci_high * condition['final_multiplier']
|
| 443 |
+
|
| 444 |
+
return {
|
| 445 |
+
'base_price': round(base_price),
|
| 446 |
+
'adjusted_price': round(adj_price),
|
| 447 |
+
'ci_low': round(adj_low),
|
| 448 |
+
'ci_high': round(adj_high),
|
| 449 |
+
'condition': condition,
|
| 450 |
+
'ai_model': 'HistGradientBoosting Regressor',
|
| 451 |
+
'accuracy_r2': '93.4%',
|
| 452 |
+
'is_model_supported': is_model_supported,
|
| 453 |
+
'market_multiplier': market_multiplier,
|
| 454 |
+
'inputs': {
|
| 455 |
+
'brand_model': orig_brand_model_str,
|
| 456 |
+
'year': year,
|
| 457 |
+
'km': int(km),
|
| 458 |
+
'fuel': fuel,
|
| 459 |
+
'transmission': trans,
|
| 460 |
+
'owner': owner,
|
| 461 |
+
'seller_type': seller,
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
# =============================================================
|
| 467 |
+
# In-memory result store (use Redis/DB in production)
|
| 468 |
+
# =============================================================
|
| 469 |
+
_results_store: dict = {}
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
# =============================================================
|
| 473 |
+
# Routes
|
| 474 |
+
# =============================================================
|
| 475 |
+
@app.route('/')
|
| 476 |
+
def home():
|
| 477 |
+
return render_template('index.html')
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
@app.route('/predict', methods=['GET'])
|
| 481 |
+
def predict_page():
|
| 482 |
+
return render_template('predict.html')
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
@app.route('/predict', methods=['POST'])
|
| 486 |
+
def predict_submit():
|
| 487 |
+
try:
|
| 488 |
+
result = predict_price(request.form.to_dict())
|
| 489 |
+
rid = str(uuid.uuid4())[:8]
|
| 490 |
+
_results_store[rid] = result
|
| 491 |
+
return redirect(url_for('result_page', rid=rid))
|
| 492 |
+
except Exception as e:
|
| 493 |
+
return render_template('predict.html', error=str(e))
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
@app.route('/result/<rid>')
|
| 497 |
+
def result_page(rid):
|
| 498 |
+
result = _results_store.get(rid)
|
| 499 |
+
if not result:
|
| 500 |
+
return redirect(url_for('predict_page'))
|
| 501 |
+
return render_template('result.html', result=result)
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
@app.route('/data-insights')
|
| 505 |
+
def data_insights():
|
| 506 |
+
return render_template('data_insights.html')
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
@app.route('/model-info')
|
| 510 |
+
def model_info():
|
| 511 |
+
return render_template('model_info.html')
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
@app.route('/about')
|
| 515 |
+
def about():
|
| 516 |
+
return render_template('about.html')
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
# JSON API (for AJAX or external consumers)
|
| 520 |
+
@app.route('/api/predict', methods=['POST'])
|
| 521 |
+
def api_predict():
|
| 522 |
+
data = request.get_json(force=True)
|
| 523 |
+
if not data:
|
| 524 |
+
return jsonify({'error': 'No JSON body'}), 400
|
| 525 |
+
try:
|
| 526 |
+
result = predict_price(data)
|
| 527 |
+
return jsonify(result)
|
| 528 |
+
except Exception as e:
|
| 529 |
+
return jsonify({'error': str(e)}), 500
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
@app.route('/api/condition-factors', methods=['GET'])
|
| 533 |
+
def api_condition_factors():
|
| 534 |
+
"""Return the full condition penalty table. Useful for frontend dropdowns."""
|
| 535 |
+
return jsonify(CONDITION_PENALTIES)
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
if __name__ == '__main__':
|
| 539 |
+
app.run(debug=True, port=5000)
|
best_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:425b2407320685db33d9fc596edf6816522e2b03d2970dd468bf0ad3e97f7a95
|
| 3 |
+
size 424792
|
brand_freq_map.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b9d2fe57114c894c0ef4bdfa4ef84712f18ff01aba01a21448ef7a7ed50e9d02
|
| 3 |
+
size 9884
|
feature_columns.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:22166d3731a63e4f06c2ca64a028754a76493805fe39ced2c3f1a276dfd32016
|
| 3 |
+
size 262
|
ordinal_encoder.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2095426de60cd1794838f106bba2625423f1e33b97cd2eab049bd20491df4778
|
| 3 |
+
size 1211
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask==3.0.3
|
| 2 |
+
joblib==1.4.2
|
| 3 |
+
numpy>=2.0.0
|
| 4 |
+
pandas>=2.2.0
|
| 5 |
+
scikit-learn>=1.6.0
|
| 6 |
+
gunicorn==22.0.0
|
| 7 |
+
|
| 8 |
+
|
static/css/style.css
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
PriceMyCar β Main Stylesheet
|
| 3 |
+
Font: Sora (UI) + DM Mono (numbers)
|
| 4 |
+
ββββββββββββββββββββββββββββββββββββββββββ */
|
| 5 |
+
:root {
|
| 6 |
+
--blue: #2563EB;
|
| 7 |
+
--blue-light: #EFF6FF;
|
| 8 |
+
--blue-dark: #1E40AF;
|
| 9 |
+
--green: #16A34A;
|
| 10 |
+
--red: #DC2626;
|
| 11 |
+
--yellow: #D97706;
|
| 12 |
+
--bg: #F8FAFC;
|
| 13 |
+
--surface: #FFFFFF;
|
| 14 |
+
--border: #E2E8F0;
|
| 15 |
+
--text: #1E293B;
|
| 16 |
+
--muted: #64748B;
|
| 17 |
+
--radius: 12px;
|
| 18 |
+
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 4px 16px rgba(0,0,0,.04);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 22 |
+
html { font-size: 16px; }
|
| 23 |
+
body { font-family: 'Sora', sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; }
|
| 24 |
+
|
| 25 |
+
/* ββ NAV ββ */
|
| 26 |
+
.navbar {
|
| 27 |
+
display: flex; align-items: center; justify-content: space-between;
|
| 28 |
+
padding: 0 40px; height: 64px; background: #fff;
|
| 29 |
+
border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 100;
|
| 30 |
+
}
|
| 31 |
+
.nav-brand { display: flex; align-items: center; gap: 10px; text-decoration: none; font-weight: 600; color: var(--text); font-size: 1rem; }
|
| 32 |
+
.nav-logo { width: 30px; height: 30px; background: var(--blue); border-radius: 8px; display: grid; place-items: center; color: #fff; font-weight: 700; font-size: .9rem; }
|
| 33 |
+
.nav-links { display: flex; gap: 32px; list-style: none; }
|
| 34 |
+
.nav-links a { text-decoration: none; color: var(--muted); font-size: .875rem; font-weight: 500; transition: color .2s; }
|
| 35 |
+
.nav-links a:hover, .nav-links a.active { color: var(--blue); }
|
| 36 |
+
.btn-primary { background: var(--blue); color: #fff; padding: 8px 20px; border-radius: 8px; font-size: .875rem; font-weight: 600; text-decoration: none; border: none; cursor: pointer; transition: background .2s; }
|
| 37 |
+
.btn-primary:hover { background: var(--blue-dark); }
|
| 38 |
+
|
| 39 |
+
/* ββ HERO ββ */
|
| 40 |
+
.hero { padding: 80px 40px 60px; max-width: 1100px; margin: 0 auto; display: flex; align-items: center; gap: 60px; }
|
| 41 |
+
.hero-text { flex: 1; }
|
| 42 |
+
.hero-badge { display: inline-flex; align-items: center; gap: 6px; background: var(--blue-light); color: var(--blue); padding: 4px 12px; border-radius: 20px; font-size: .75rem; font-weight: 600; margin-bottom: 20px; }
|
| 43 |
+
.hero h1 { font-size: 2.75rem; font-weight: 700; line-height: 1.2; margin-bottom: 16px; }
|
| 44 |
+
.hero h1 span { color: var(--blue); }
|
| 45 |
+
.hero p { color: var(--muted); margin-bottom: 28px; font-size: 1rem; max-width: 420px; }
|
| 46 |
+
.hero-btns { display: flex; gap: 12px; }
|
| 47 |
+
.btn-outline { background: #fff; color: var(--text); padding: 10px 22px; border-radius: 8px; font-size: .875rem; font-weight: 600; text-decoration: none; border: 1.5px solid var(--border); transition: border-color .2s; }
|
| 48 |
+
.btn-outline:hover { border-color: var(--blue); color: var(--blue); }
|
| 49 |
+
.hero-img { flex: 0 0 340px; }
|
| 50 |
+
.hero-img svg { width: 100%; }
|
| 51 |
+
|
| 52 |
+
/* ββ WHY SECTION ββ */
|
| 53 |
+
.why-section { background: var(--blue-light); padding: 80px 40px; text-align: center; }
|
| 54 |
+
.section-heading { font-size: 1.75rem; font-weight: 700; margin-bottom: 8px; }
|
| 55 |
+
.section-sub { color: var(--muted); margin-bottom: 48px; }
|
| 56 |
+
.cards-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; max-width: 960px; margin: 0 auto; }
|
| 57 |
+
.card { background: #fff; border-radius: var(--radius); padding: 28px; text-align: left; box-shadow: var(--shadow); }
|
| 58 |
+
.card-icon { font-size: 1.5rem; margin-bottom: 12px; }
|
| 59 |
+
.card h3 { font-size: 1rem; font-weight: 600; margin-bottom: 8px; }
|
| 60 |
+
.card p { font-size: .875rem; color: var(--muted); }
|
| 61 |
+
|
| 62 |
+
/* ββ HOW IT WORKS ββ */
|
| 63 |
+
.how-section { padding: 80px 40px; text-align: center; }
|
| 64 |
+
.steps-grid { display: flex; gap: 0; max-width: 700px; margin: 40px auto 0; position: relative; }
|
| 65 |
+
.steps-grid::before { content: ''; position: absolute; top: 24px; left: 48px; right: 48px; height: 2px; background: var(--border); z-index: 0; }
|
| 66 |
+
.step { flex: 1; text-align: center; position: relative; z-index: 1; }
|
| 67 |
+
.step-num { width: 48px; height: 48px; border-radius: 50%; background: var(--blue); color: #fff; font-weight: 700; display: grid; place-items: center; margin: 0 auto 16px; font-size: 1rem; }
|
| 68 |
+
.step h3 { font-size: .9rem; font-weight: 600; margin-bottom: 6px; }
|
| 69 |
+
.step p { font-size: .8rem; color: var(--muted); }
|
| 70 |
+
|
| 71 |
+
/* ββ PREDICT PAGE ββ */
|
| 72 |
+
.predict-page { padding: 48px 40px; max-width: 1100px; margin: 0 auto; }
|
| 73 |
+
.predict-page h1 { font-size: 1.875rem; font-weight: 700; margin-bottom: 6px; }
|
| 74 |
+
.page-sub { color: var(--muted); margin-bottom: 32px; }
|
| 75 |
+
.predict-container { display: grid; grid-template-columns: 1fr 300px; gap: 32px; align-items: start; }
|
| 76 |
+
.form-section { background: #fff; border-radius: var(--radius); padding: 28px; margin-bottom: 20px; border: 1px solid var(--border); }
|
| 77 |
+
.section-title { font-weight: 600; font-size: 1rem; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; }
|
| 78 |
+
.section-icon { font-size: 1.1rem; }
|
| 79 |
+
.badge-new { background: var(--blue); color: #fff; font-size: .65rem; padding: 2px 8px; border-radius: 10px; font-weight: 700; }
|
| 80 |
+
.section-desc { font-size: .8rem; color: var(--muted); margin-bottom: 20px; line-height: 1.5; padding: 10px 14px; background: var(--blue-light); border-radius: 8px; }
|
| 81 |
+
.form-row { display: flex; gap: 16px; margin-bottom: 16px; }
|
| 82 |
+
.form-row.two-col > * { flex: 1; }
|
| 83 |
+
.form-group { display: flex; flex-direction: column; }
|
| 84 |
+
.form-group label { font-size: .8rem; font-weight: 600; color: var(--muted); margin-bottom: 6px; }
|
| 85 |
+
.form-group input, .form-group select {
|
| 86 |
+
padding: 10px 14px; border: 1.5px solid var(--border); border-radius: 8px;
|
| 87 |
+
font-family: 'Sora', sans-serif; font-size: .875rem; color: var(--text);
|
| 88 |
+
background: #fff; transition: border-color .2s;
|
| 89 |
+
}
|
| 90 |
+
.form-group input:focus, .form-group select:focus { outline: none; border-color: var(--blue); }
|
| 91 |
+
.form-group small { font-size: .72rem; color: var(--muted); margin-top: 4px; }
|
| 92 |
+
.condition-section { border: 1.5px solid var(--blue); background: #FAFCFF; }
|
| 93 |
+
.penalty-preview { margin-top: 20px; padding: 14px; background: #fff; border-radius: 8px; border: 1px solid var(--border); }
|
| 94 |
+
.penalty-bar-wrap { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: .8rem; font-weight: 500; }
|
| 95 |
+
.penalty-pct { color: var(--red); font-family: 'DM Mono', monospace; font-weight: 700; }
|
| 96 |
+
.penalty-bar { height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; }
|
| 97 |
+
.penalty-fill { height: 100%; background: var(--red); border-radius: 4px; transition: width .4s; }
|
| 98 |
+
.btn-predict { width: 100%; padding: 16px; background: var(--blue); color: #fff; font-family: 'Sora', sans-serif; font-size: 1rem; font-weight: 600; border: none; border-radius: 10px; cursor: pointer; transition: background .2s; }
|
| 99 |
+
.btn-predict:hover { background: var(--blue-dark); }
|
| 100 |
+
|
| 101 |
+
/* sidebar */
|
| 102 |
+
.predict-sidebar { position: sticky; top: 80px; }
|
| 103 |
+
.sidebar-card { background: #fff; border-radius: var(--radius); padding: 20px; border: 1px solid var(--border); margin-bottom: 16px; }
|
| 104 |
+
.sidebar-card h3 { font-size: .9rem; font-weight: 600; margin-bottom: 14px; }
|
| 105 |
+
.progress-wrap { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
|
| 106 |
+
.progress-bar { flex: 1; height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; }
|
| 107 |
+
.progress-fill { height: 100%; background: var(--blue); border-radius: 4px; transition: width .4s; }
|
| 108 |
+
.progress-wrap span { font-size: .8rem; font-weight: 700; color: var(--blue); font-family: 'DM Mono', monospace; min-width: 36px; }
|
| 109 |
+
.completeness-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
|
| 110 |
+
.completeness-list li { font-size: .8rem; padding-left: 20px; position: relative; color: var(--muted); }
|
| 111 |
+
.completeness-list li::before { content: 'β'; position: absolute; left: 0; color: var(--muted); }
|
| 112 |
+
.completeness-list li.done { color: var(--green); }
|
| 113 |
+
.completeness-list li.done::before { content: 'β'; color: var(--green); }
|
| 114 |
+
.how-card p { font-size: .8rem; color: var(--muted); }
|
| 115 |
+
|
| 116 |
+
/* ββ RESULT PAGE ββ */
|
| 117 |
+
.result-page { padding: 48px 40px; max-width: 1100px; margin: 0 auto; }
|
| 118 |
+
.back-link { font-size: .8rem; color: var(--muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px; margin-bottom: 20px; }
|
| 119 |
+
.back-link:hover { color: var(--blue); }
|
| 120 |
+
.result-page h1 { font-size: 1.875rem; font-weight: 700; margin-bottom: 4px; }
|
| 121 |
+
.result-grid { display: grid; grid-template-columns: 1fr 360px; gap: 28px; margin-top: 28px; }
|
| 122 |
+
.price-card { background: #fff; border-radius: var(--radius); padding: 32px; border: 1px solid var(--border); margin-bottom: 20px; }
|
| 123 |
+
.confidence-badge { display: inline-flex; align-items: center; gap: 6px; background: #D1FAE5; color: var(--green); padding: 4px 12px; border-radius: 20px; font-size: .75rem; font-weight: 600; margin-bottom: 20px; }
|
| 124 |
+
.confidence-badge::before { content: 'β'; font-size: .6rem; }
|
| 125 |
+
.price-label { font-size: .8rem; color: var(--muted); margin-bottom: 8px; }
|
| 126 |
+
.price-main { font-size: 2.5rem; font-weight: 700; color: var(--text); font-family: 'DM Mono', monospace; }
|
| 127 |
+
.price-adjustment { display: flex; align-items: center; gap: 10px; margin-top: 8px; flex-wrap: wrap; }
|
| 128 |
+
.base-label { font-size: .8rem; color: var(--muted); }
|
| 129 |
+
.penalty-tag { background: #FEF2F2; color: var(--red); padding: 2px 10px; border-radius: 20px; font-size: .75rem; font-weight: 600; }
|
| 130 |
+
.price-range { font-size: .8rem; color: var(--muted); margin-top: 12px; font-family: 'DM Mono', monospace; }
|
| 131 |
+
.chart-card { background: #fff; border-radius: var(--radius); padding: 24px; border: 1px solid var(--border); }
|
| 132 |
+
.chart-card h3 { font-size: .9rem; font-weight: 600; margin-bottom: 20px; }
|
| 133 |
+
.bar-chart { display: flex; align-items: flex-end; gap: 20px; height: 120px; }
|
| 134 |
+
.bar-group { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 8px; height: 100%; justify-content: flex-end; }
|
| 135 |
+
.bar { width: 100%; background: #CBD5E1; border-radius: 4px 4px 0 0; transition: height .5s; }
|
| 136 |
+
.bar.highlight { background: var(--blue); }
|
| 137 |
+
.bar-group span { font-size: .7rem; color: var(--muted); text-align: center; }
|
| 138 |
+
.factors-card { background: #fff; border-radius: var(--radius); padding: 20px; border: 1px solid var(--border); margin-bottom: 16px; }
|
| 139 |
+
.factors-card h3 { font-size: .9rem; font-weight: 600; margin-bottom: 14px; }
|
| 140 |
+
.factor { padding: 12px 14px; border-radius: 8px; margin-bottom: 10px; font-size: .8rem; }
|
| 141 |
+
.factor.positive { background: #F0FDF4; border-left: 3px solid var(--green); }
|
| 142 |
+
.factor.negative { background: #FFF7ED; border-left: 3px solid var(--yellow); }
|
| 143 |
+
.factor-label { font-weight: 600; margin-bottom: 4px; font-size: .75rem; }
|
| 144 |
+
.breakdown-table { width: 100%; border-collapse: collapse; font-size: .78rem; }
|
| 145 |
+
.breakdown-table th { text-align: left; padding: 6px 8px; border-bottom: 1.5px solid var(--border); color: var(--muted); font-weight: 600; }
|
| 146 |
+
.breakdown-table td { padding: 6px 8px; border-bottom: 1px solid var(--border); }
|
| 147 |
+
.breakdown-table .total-row td { border-top: 1.5px solid var(--border); border-bottom: none; }
|
| 148 |
+
.penalty-neg { color: var(--red); font-family: 'DM Mono', monospace; font-weight: 600; }
|
| 149 |
+
.penalty-pos { color: var(--green); font-family: 'DM Mono', monospace; font-weight: 600; }
|
| 150 |
+
|
| 151 |
+
/* ββ DATA INSIGHTS PAGE ββ */
|
| 152 |
+
.insights-page { padding: 48px 40px; max-width: 1100px; margin: 0 auto; }
|
| 153 |
+
.insights-page h1 { font-size: 1.875rem; font-weight: 700; margin-bottom: 6px; }
|
| 154 |
+
.stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 28px 0; }
|
| 155 |
+
.stat-card { background: #fff; border-radius: var(--radius); padding: 20px 24px; border: 1px solid var(--border); }
|
| 156 |
+
.stat-label { font-size: .75rem; color: var(--muted); font-weight: 600; margin-bottom: 6px; }
|
| 157 |
+
.stat-value { font-size: 1.75rem; font-weight: 700; font-family: 'DM Mono', monospace; }
|
| 158 |
+
.charts-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
|
| 159 |
+
.chart-wrap { background: #fff; border-radius: var(--radius); padding: 24px; border: 1px solid var(--border); }
|
| 160 |
+
.chart-wrap h3 { font-size: .9rem; font-weight: 600; margin-bottom: 16px; }
|
| 161 |
+
|
| 162 |
+
/* ββ MODEL INFO PAGE ββ */
|
| 163 |
+
.model-page { padding: 48px 40px 80px; max-width: 900px; margin: 0 auto; text-align: center; }
|
| 164 |
+
.model-page h1 { font-size: 2rem; font-weight: 700; margin-bottom: 10px; }
|
| 165 |
+
.pipeline { display: flex; gap: 0; margin: 48px 0 64px; position: relative; }
|
| 166 |
+
.pipeline::before { content: ''; position: absolute; top: 28px; left: 64px; right: 64px; height: 2px; background: var(--border); }
|
| 167 |
+
.pipe-step { flex: 1; text-align: center; position: relative; z-index: 1; }
|
| 168 |
+
.pipe-icon { width: 56px; height: 56px; border-radius: var(--radius); background: #fff; border: 1.5px solid var(--border); display: grid; place-items: center; margin: 0 auto 12px; font-size: 1.3rem; }
|
| 169 |
+
.pipe-step.active .pipe-icon { background: var(--blue); border-color: var(--blue); }
|
| 170 |
+
.pipe-step h4 { font-size: .8rem; font-weight: 600; margin-bottom: 4px; }
|
| 171 |
+
.pipe-step p { font-size: .73rem; color: var(--muted); }
|
| 172 |
+
.algo-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; margin-top: 48px; text-align: left; }
|
| 173 |
+
.algo-section h2, .metrics-section h2 { font-size: 1.25rem; font-weight: 700; margin-bottom: 16px; }
|
| 174 |
+
.algo-section p { font-size: .875rem; color: var(--muted); margin-bottom: 12px; }
|
| 175 |
+
.feature-list { list-style: none; display: flex; flex-direction: column; gap: 8px; margin-top: 16px; }
|
| 176 |
+
.feature-list li { display: flex; align-items: center; gap: 8px; font-size: .875rem; }
|
| 177 |
+
.feature-list li::before { content: 'β'; color: var(--green); font-weight: 700; }
|
| 178 |
+
.metric-row { background: #fff; border-radius: var(--radius); padding: 18px 20px; border: 1px solid var(--border); display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
| 179 |
+
.metric-name { font-weight: 600; font-size: .9rem; margin-bottom: 4px; }
|
| 180 |
+
.metric-desc { font-size: .78rem; color: var(--muted); }
|
| 181 |
+
.metric-val { font-size: 1.25rem; font-weight: 700; font-family: 'DM Mono', monospace; color: var(--blue); }
|
| 182 |
+
|
| 183 |
+
/* ββ ABOUT PAGE ββ */
|
| 184 |
+
.about-page { padding: 64px 40px 80px; max-width: 900px; margin: 0 auto; text-align: center; }
|
| 185 |
+
.about-page h1 { font-size: 2rem; font-weight: 700; margin-bottom: 10px; }
|
| 186 |
+
.about-body { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin: 48px 0; text-align: left; }
|
| 187 |
+
.about-left h2, .dataset-card h2 { font-size: 1.1rem; font-weight: 700; margin-bottom: 10px; }
|
| 188 |
+
.about-left p { font-size: .875rem; color: var(--muted); line-height: 1.7; }
|
| 189 |
+
.dataset-card { background: #fff; border-radius: var(--radius); padding: 24px; border: 1px solid var(--border); height: fit-content; }
|
| 190 |
+
.dataset-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--border); font-size: .875rem; }
|
| 191 |
+
.dataset-row:last-child { border: none; }
|
| 192 |
+
.dataset-row span:last-child { font-weight: 600; }
|
| 193 |
+
.team-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; }
|
| 194 |
+
.team-card { background: #fff; border-radius: var(--radius); padding: 24px; border: 1px solid var(--border); text-align: center; }
|
| 195 |
+
.team-avatar { width: 64px; height: 64px; border-radius: 50%; background: var(--blue-light); display: grid; place-items: center; margin: 0 auto 12px; font-size: 1.5rem; }
|
| 196 |
+
.team-card h4 { font-weight: 600; font-size: .9rem; margin-bottom: 4px; }
|
| 197 |
+
.team-card span { font-size: .78rem; color: var(--blue); }
|
| 198 |
+
|
| 199 |
+
/* ββ FOOTER ββ */
|
| 200 |
+
.footer { background: #fff; border-top: 1px solid var(--border); padding: 48px 40px 24px; }
|
| 201 |
+
.footer-inner { display: flex; gap: 64px; max-width: 1100px; margin: 0 auto 32px; }
|
| 202 |
+
.footer-brand { flex: 2; }
|
| 203 |
+
.footer-brand p { font-size: .8rem; color: var(--muted); margin-top: 12px; line-height: 1.6; }
|
| 204 |
+
.footer-col h4 { font-size: .8rem; font-weight: 700; margin-bottom: 14px; }
|
| 205 |
+
.footer-col ul { list-style: none; display: flex; flex-direction: column; gap: 8px; }
|
| 206 |
+
.footer-col a { font-size: .8rem; color: var(--muted); text-decoration: none; }
|
| 207 |
+
.footer-col a:hover { color: var(--blue); }
|
| 208 |
+
.footer-copy { text-align: center; font-size: .75rem; color: var(--muted); max-width: 1100px; margin: 0 auto; padding-top: 24px; border-top: 1px solid var(--border); }
|
| 209 |
+
|
| 210 |
+
/* ββ ALERT ββ */
|
| 211 |
+
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: .875rem; display: flex; gap: 12px; align-items: flex-start; text-align: left; }
|
| 212 |
+
.alert-error { background: #FEF2F2; color: var(--red); border: 1px solid #FECACA; }
|
| 213 |
+
.alert-warning { background: #FFFBEB; color: #92400E; border: 1px solid #FDE68A; }
|
| 214 |
+
.alert-warning h4 { font-weight: 600; margin-bottom: 4px; font-size: .9rem; color: #78350F; }
|
| 215 |
+
.alert-warning p { font-size: .82rem; color: #92400E; margin: 0; line-height: 1.4; }
|
| 216 |
+
.alert-icon { font-size: 1.25rem; }
|
| 217 |
+
|
| 218 |
+
/* ββ MODELS CARD (PREDICT PAGE SIDEBAR) ββ */
|
| 219 |
+
.models-details { font-size: .8rem; color: var(--muted); cursor: pointer; }
|
| 220 |
+
.models-details summary { font-weight: 600; color: var(--blue); margin-bottom: 6px; outline: none; }
|
| 221 |
+
.supported-models-list { font-size: .75rem; line-height: 1.5; color: var(--text); background: var(--bg); padding: 12px; border-radius: 6px; border: 1px solid var(--border); margin-top: 6px; max-height: 200px; overflow-y: auto; text-align: left; }
|
| 222 |
+
|
| 223 |
+
/* ββ ANALYSIS CARD (RESULT PAGE) ββ */
|
| 224 |
+
.analysis-card { background: #fff; border-radius: var(--radius); padding: 24px; border: 1px solid var(--border); margin-top: 20px; text-align: left; }
|
| 225 |
+
.analysis-card h3 { font-size: 1rem; font-weight: 700; margin-bottom: 20px; color: var(--text); }
|
| 226 |
+
.analysis-section { margin-bottom: 20px; }
|
| 227 |
+
.analysis-section:last-child { margin-bottom: 0; }
|
| 228 |
+
.analysis-section h4 { font-size: .85rem; font-weight: 600; margin-bottom: 8px; color: var(--blue); }
|
| 229 |
+
.analysis-section p { font-size: .8rem; color: var(--muted); line-height: 1.6; margin-bottom: 12px; }
|
| 230 |
+
.deviation-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 12px; }
|
| 231 |
+
.dev-item { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 12px; position: relative; }
|
| 232 |
+
.dev-item strong { display: block; font-size: .78rem; font-weight: 600; color: var(--text); margin-bottom: 4px; padding-left: 24px; }
|
| 233 |
+
.dev-icon { position: absolute; left: 12px; top: 12px; font-size: .95rem; }
|
| 234 |
+
.dev-item p { font-size: .72rem; color: var(--muted); line-height: 1.4; margin: 0; padding-left: 24px; }
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
/* ββ RESPONSIVE DESIGN (MEDIA QUERIES) ββ */
|
| 238 |
+
@media (max-width: 992px) {
|
| 239 |
+
.predict-container {
|
| 240 |
+
grid-template-columns: 1fr;
|
| 241 |
+
}
|
| 242 |
+
.result-grid {
|
| 243 |
+
grid-template-columns: 1fr;
|
| 244 |
+
}
|
| 245 |
+
.predict-sidebar {
|
| 246 |
+
position: static;
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
@media (max-width: 768px) {
|
| 251 |
+
html { font-size: 15px; }
|
| 252 |
+
|
| 253 |
+
.navbar {
|
| 254 |
+
padding: 12px 20px;
|
| 255 |
+
height: auto;
|
| 256 |
+
flex-direction: column;
|
| 257 |
+
gap: 12px;
|
| 258 |
+
}
|
| 259 |
+
.nav-links {
|
| 260 |
+
gap: 16px;
|
| 261 |
+
flex-wrap: wrap;
|
| 262 |
+
justify-content: center;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.hero {
|
| 266 |
+
flex-direction: column-reverse;
|
| 267 |
+
padding: 40px 20px;
|
| 268 |
+
gap: 32px;
|
| 269 |
+
text-align: center;
|
| 270 |
+
}
|
| 271 |
+
.hero h1 {
|
| 272 |
+
font-size: 2.2rem;
|
| 273 |
+
}
|
| 274 |
+
.hero p {
|
| 275 |
+
margin: 0 auto 24px;
|
| 276 |
+
}
|
| 277 |
+
.hero-btns {
|
| 278 |
+
justify-content: center;
|
| 279 |
+
}
|
| 280 |
+
.hero-img {
|
| 281 |
+
flex: 0 0 auto;
|
| 282 |
+
width: 200px;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.why-section, .how-section, .predict-page, .result-page, .insights-page, .model-page, .about-page {
|
| 286 |
+
padding: 40px 20px;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.cards-grid {
|
| 290 |
+
grid-template-columns: 1fr;
|
| 291 |
+
gap: 20px;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.steps-grid {
|
| 295 |
+
flex-direction: column;
|
| 296 |
+
gap: 32px;
|
| 297 |
+
}
|
| 298 |
+
.steps-grid::before {
|
| 299 |
+
display: none;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.form-row {
|
| 303 |
+
flex-direction: column;
|
| 304 |
+
gap: 16px;
|
| 305 |
+
}
|
| 306 |
+
.form-row.two-col > * {
|
| 307 |
+
flex: none;
|
| 308 |
+
width: 100%;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.charts-row {
|
| 312 |
+
grid-template-columns: 1fr;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.deviation-grid {
|
| 316 |
+
grid-template-columns: 1fr;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.stats-row {
|
| 320 |
+
grid-template-columns: 1fr;
|
| 321 |
+
gap: 16px;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.about-body {
|
| 325 |
+
grid-template-columns: 1fr;
|
| 326 |
+
gap: 32px;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.team-grid {
|
| 330 |
+
grid-template-columns: repeat(2, 1fr);
|
| 331 |
+
gap: 16px;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.footer-inner {
|
| 335 |
+
flex-direction: column;
|
| 336 |
+
gap: 32px;
|
| 337 |
+
text-align: center;
|
| 338 |
+
}
|
| 339 |
+
.footer-brand {
|
| 340 |
+
text-align: center;
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
@media (max-width: 480px) {
|
| 345 |
+
.team-grid {
|
| 346 |
+
grid-template-columns: 1fr;
|
| 347 |
+
}
|
| 348 |
+
.hero-btns {
|
| 349 |
+
flex-direction: column;
|
| 350 |
+
width: 100%;
|
| 351 |
+
}
|
| 352 |
+
.btn-primary, .btn-outline {
|
| 353 |
+
text-align: center;
|
| 354 |
+
width: 100%;
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
|
static/js/main.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// main.js β global utilities
|
| 2 |
+
|
| 3 |
+
// Highlight active nav link
|
| 4 |
+
document.querySelectorAll('.nav-links a').forEach(a => {
|
| 5 |
+
if (a.href === window.location.href) a.classList.add('active');
|
| 6 |
+
});
|
static/js/predict.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
// predict.js β live penalty preview + progress
|
| 3 |
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
|
| 5 |
+
const PENALTIES = {
|
| 6 |
+
body_damage_severity: { 0: 0, 1: 4, 2: 12, 3: 28 },
|
| 7 |
+
paint_condition: { excellent: 0, good: 2, fair: 6, poor: 13 },
|
| 8 |
+
interior_condition: { excellent: 0, good: 2, fair: 7, poor: 15 },
|
| 9 |
+
accident_history: { none: 0, minor: 8, moderate: 20, major: 40 },
|
| 10 |
+
flood_damage: { none: 0, minor: 20, severe: 50 },
|
| 11 |
+
engine_condition: { excellent: 0, good: 3, fair: 10, poor: 30 },
|
| 12 |
+
tire_condition: { good: 0, worn: 3, bald: 5 },
|
| 13 |
+
service_history: { complete: -3, partial: 0, none: 6 },
|
| 14 |
+
modification_status: { stock: 0, cosmetic_minor: 2, cosmetic_major: 5, performance: 4, non_reversible: 8 },
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
function calcPenalty() {
|
| 18 |
+
let total = 0;
|
| 19 |
+
|
| 20 |
+
// Body damage severity
|
| 21 |
+
const sev = parseInt(document.querySelector('[name=body_damage_severity]')?.value || 0);
|
| 22 |
+
total += PENALTIES.body_damage_severity[sev] || 0;
|
| 23 |
+
|
| 24 |
+
// Dent count extra
|
| 25 |
+
const dents = parseInt(document.querySelector('[name=dent_count]')?.value || 0);
|
| 26 |
+
total += Math.min(dents * 2, 15);
|
| 27 |
+
|
| 28 |
+
// Other dropdowns
|
| 29 |
+
const fields = ['paint_condition','interior_condition','accident_history',
|
| 30 |
+
'flood_damage','engine_condition','tire_condition',
|
| 31 |
+
'service_history','modification_status'];
|
| 32 |
+
fields.forEach(f => {
|
| 33 |
+
const el = document.querySelector(`[name=${f}]`);
|
| 34 |
+
if (el) total += PENALTIES[f][el.value] || 0;
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
return Math.max(-5, Math.min(90, total));
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
function updatePenaltyPreview() {
|
| 41 |
+
const pct = calcPenalty();
|
| 42 |
+
document.getElementById('penaltyPct').textContent = (pct >= 0 ? 'β' : '+') + Math.abs(pct) + '%';
|
| 43 |
+
document.getElementById('penaltyFill').style.width = Math.abs(pct) + '%';
|
| 44 |
+
const fill = document.getElementById('penaltyFill');
|
| 45 |
+
fill.style.background = pct > 20 ? '#DC2626' : pct > 10 ? '#D97706' : '#16A34A';
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// ββ Progress tracker ββ
|
| 49 |
+
function updateProgress() {
|
| 50 |
+
const brand = document.querySelector('[name=brand]')?.value.trim();
|
| 51 |
+
const model = document.querySelector('[name=model]')?.value.trim();
|
| 52 |
+
const year = document.querySelector('[name=year]')?.value;
|
| 53 |
+
const km = document.querySelector('[name=mileage]')?.value;
|
| 54 |
+
const fuel = document.querySelector('[name=fuel_type]')?.value;
|
| 55 |
+
|
| 56 |
+
const checks = {
|
| 57 |
+
'cl-make': !!(brand && model),
|
| 58 |
+
'cl-age': !!(year && km),
|
| 59 |
+
'cl-spec': !!(fuel),
|
| 60 |
+
'cl-cond': calcPenalty() !== 5, // user touched condition section
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
let done = 0;
|
| 64 |
+
Object.entries(checks).forEach(([id, ok]) => {
|
| 65 |
+
const el = document.getElementById(id);
|
| 66 |
+
if (el) { el.classList.toggle('done', ok); if (ok) done++; }
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
const pct = 14 + Math.round((done / 4) * 86);
|
| 70 |
+
document.getElementById('progressFill').style.width = pct + '%';
|
| 71 |
+
document.getElementById('progressPct').textContent = pct + '%';
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// ββ Event Listeners ββ
|
| 75 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 76 |
+
document.getElementById('predictForm')?.addEventListener('input', () => {
|
| 77 |
+
updatePenaltyPreview();
|
| 78 |
+
updateProgress();
|
| 79 |
+
});
|
| 80 |
+
updatePenaltyPreview();
|
| 81 |
+
updateProgress();
|
| 82 |
+
});
|
templates/about.html
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}About - PriceMyCar{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<section class="about-page">
|
| 5 |
+
<h1>About PriceMyCar</h1>
|
| 6 |
+
<p class="section-sub">Democratizing automotive data to make buying and selling used cars transparent and fair.</p>
|
| 7 |
+
|
| 8 |
+
<div class="about-body">
|
| 9 |
+
<div class="about-left">
|
| 10 |
+
<h2>The Problem</h2>
|
| 11 |
+
<p>The used car market is notoriously opaque. Buyers often overpay due to information asymmetry, while sellers struggle to price their vehicles competitively without leaving money on the table.</p>
|
| 12 |
+
<h2 style="margin-top:24px">Our Solution</h2>
|
| 13 |
+
<p>We built PriceMyCar to bridge this gap. By leveraging a dataset of 4,340 historical transactions and applying advanced machine learning techniques (HistGradientBoosting Regressor + 10-factor condition scoring), we provide real-time, highly accurate valuations that reflect the true state of the market today.</p>
|
| 14 |
+
<h2 style="margin-top:24px">Contribution</h2>
|
| 15 |
+
<p>The <strong>Condition Adjustment System</strong> was proposed to address a key limitation: ML models trained on listing data cannot account for physical state at point-of-sale. Our 10-factor penalty model fills this gap using empirical depreciation research.</p>
|
| 16 |
+
</div>
|
| 17 |
+
<div class="dataset-card">
|
| 18 |
+
<h2>The Dataset</h2>
|
| 19 |
+
<p style="font-size:.8rem;color:#64748B;margin-bottom:16px">CarDekho India used car listings (Kaggle)</p>
|
| 20 |
+
<div class="dataset-row"><span>Total Records</span><span>4,340</span></div>
|
| 21 |
+
<div class="dataset-row"><span>Date Range</span><span>2001-2021</span></div>
|
| 22 |
+
<div class="dataset-row"><span>Base Features</span><span>8 variables</span></div>
|
| 23 |
+
<div class="dataset-row"><span>Engineered Features</span><span>17 total</span></div>
|
| 24 |
+
<div class="dataset-row"><span>Best Model</span><span>HistGradientBoosting Regressor</span></div>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<h2 class="section-heading" style="margin-bottom:24px">Meet the Team</h2>
|
| 29 |
+
<div class="team-grid">
|
| 30 |
+
<div class="team-card"><div class="team-avatar"></div><h4>ALDEEZA PRADITHA EFENDI</h4><span>Student @ BINUS University</span></div>
|
| 31 |
+
<div class="team-card"><div class="team-avatar"></div><h4>CRISWINCENT ENRICO GERALDY</h4><span>Student @ BINUS University</span></div>
|
| 32 |
+
<div class="team-card"><div class="team-avatar"></div><h4>GREGORIO KEEFE JASON S</h4><span>Student @ BINUS University</span></div>
|
| 33 |
+
<div class="team-card"><div class="team-avatar"></div><h4>RAFAEL SACCHI</h4><span>Student @ BINUS University</span></div>
|
| 34 |
+
</div>
|
| 35 |
+
</section>
|
| 36 |
+
{% endblock %}
|
templates/base.html
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8"/>
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
+
<title>{% block title %}PriceMyCar{% endblock %}</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 9 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 10 |
+
{% block extra_head %}{% endblock %}
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
|
| 14 |
+
<!-- ββ NAV βββββββββββββββββββββββββββββββ -->
|
| 15 |
+
<nav class="navbar">
|
| 16 |
+
<a href="{{ url_for('home') }}" class="nav-brand">
|
| 17 |
+
<div class="nav-logo">P</div>
|
| 18 |
+
<span>PriceMyCar</span>
|
| 19 |
+
</a>
|
| 20 |
+
<ul class="nav-links">
|
| 21 |
+
<li><a href="{{ url_for('home') }}" class="{% if request.endpoint == 'home' %}active{% endif %}">Home</a></li>
|
| 22 |
+
<li><a href="{{ url_for('predict_page') }}" class="{% if request.endpoint in ['predict_page','predict_submit'] %}active{% endif %}">Predict</a></li>
|
| 23 |
+
<li><a href="{{ url_for('data_insights') }}" class="{% if request.endpoint == 'data_insights' %}active{% endif %}">Data Insights</a></li>
|
| 24 |
+
<li><a href="{{ url_for('model_info') }}" class="{% if request.endpoint == 'model_info' %}active{% endif %}">Model Info</a></li>
|
| 25 |
+
<li><a href="{{ url_for('about') }}" class="{% if request.endpoint == 'about' %}active{% endif %}">About</a></li>
|
| 26 |
+
</ul>
|
| 27 |
+
<a href="{{ url_for('predict_page') }}" class="btn-primary">Get Started</a>
|
| 28 |
+
</nav>
|
| 29 |
+
|
| 30 |
+
<!-- ββ PAGE CONTENT ββββββββββββββββββββββ -->
|
| 31 |
+
<main>
|
| 32 |
+
{% block content %}{% endblock %}
|
| 33 |
+
</main>
|
| 34 |
+
|
| 35 |
+
<!-- ββ FOOTER ββββββββββββββββββββββββββββ -->
|
| 36 |
+
<footer class="footer">
|
| 37 |
+
<div class="footer-inner">
|
| 38 |
+
<div class="footer-brand">
|
| 39 |
+
<div class="nav-brand">
|
| 40 |
+
<div class="nav-logo">P</div>
|
| 41 |
+
<span>PriceMyCar</span>
|
| 42 |
+
</div>
|
| 43 |
+
<p>Empowering buyers and sellers with highly accurate,<br>machine learning-driven used car price predictions.</p>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="footer-col">
|
| 46 |
+
<h4>Product</h4>
|
| 47 |
+
<ul>
|
| 48 |
+
<li><a href="{{ url_for('predict_page') }}">Predict Price</a></li>
|
| 49 |
+
<li><a href="{{ url_for('data_insights') }}">Data Insights</a></li>
|
| 50 |
+
<li><a href="{{ url_for('model_info') }}">How It Works</a></li>
|
| 51 |
+
</ul>
|
| 52 |
+
</div>
|
| 53 |
+
<div class="footer-col">
|
| 54 |
+
<h4>Company</h4>
|
| 55 |
+
<ul>
|
| 56 |
+
<li><a href="{{ url_for('about') }}">About Us</a></li>
|
| 57 |
+
<li><a href="#">Privacy Policy</a></li>
|
| 58 |
+
<li><a href="#">Terms of Service</a></li>
|
| 59 |
+
</ul>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
<div class="footer-copy">Β© 2025 PriceMyCar. All rights reserved.</div>
|
| 63 |
+
</footer>
|
| 64 |
+
|
| 65 |
+
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
| 66 |
+
{% block extra_js %}{% endblock %}
|
| 67 |
+
</body>
|
| 68 |
+
</html>
|
templates/data_insights.html
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Data Insights β PriceMyCar{% endblock %}
|
| 3 |
+
{% block extra_head %}<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>{% endblock %}
|
| 4 |
+
{% block content %}
|
| 5 |
+
<section class="insights-page">
|
| 6 |
+
<h1>Market Insights</h1>
|
| 7 |
+
<p class="page-sub">Explore trends and patterns from our dataset of 4,300+ vehicles.</p>
|
| 8 |
+
|
| 9 |
+
<div class="stats-row">
|
| 10 |
+
<div class="stat-card"><div class="stat-label">π Total Records</div><div class="stat-value">4,340</div></div>
|
| 11 |
+
<div class="stat-card"><div class="stat-label">π Avg. 3-Year Depreciation</div><div class="stat-value">31.2%</div></div>
|
| 12 |
+
<div class="stat-card"><div class="stat-label">π Market Volatility</div><div class="stat-value" style="font-size:1.3rem;color:var(--green)">Low</div></div>
|
| 13 |
+
</div>
|
| 14 |
+
|
| 15 |
+
<div class="charts-row">
|
| 16 |
+
<div class="chart-wrap"><h3>Average Depreciation Curve</h3><canvas id="deprecChart" height="180"></canvas></div>
|
| 17 |
+
<div class="chart-wrap"><h3>Price Impact by Mileage</h3><canvas id="mileageChart" height="180"></canvas></div>
|
| 18 |
+
</div>
|
| 19 |
+
<div class="chart-wrap" style="margin-bottom:20px; display: flex; flex-direction: column; align-items: center;">
|
| 20 |
+
<h3>Dataset Brand Distribution</h3>
|
| 21 |
+
<div style="max-width: 340px; width: 100%; margin: 0 auto;">
|
| 22 |
+
<canvas id="brandChart" height="280"></canvas>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
</section>
|
| 26 |
+
{% endblock %}
|
| 27 |
+
{% block extra_js %}
|
| 28 |
+
<script>
|
| 29 |
+
const blue = '#2563EB', lblue = '#BFDBFE', muted = '#94A3B8';
|
| 30 |
+
// Depreciation curve
|
| 31 |
+
new Chart(document.getElementById('deprecChart'), {
|
| 32 |
+
type:'line', data:{
|
| 33 |
+
labels:['Year 1','Year 2','Year 3','Year 4','Year 5','Year 6','Year 7'],
|
| 34 |
+
datasets:[{label:'Value Retained %',data:[100,82,70,61,54,48,43],borderColor:blue,backgroundColor:lblue+'44',fill:true,tension:.4,pointBackgroundColor:blue}]
|
| 35 |
+
}, options:{plugins:{legend:{display:false}},scales:{y:{min:0,max:110}}}
|
| 36 |
+
});
|
| 37 |
+
// Price by mileage
|
| 38 |
+
new Chart(document.getElementById('mileageChart'), {
|
| 39 |
+
type:'bar', data:{
|
| 40 |
+
labels:['0β20k','20β40k','40β60k','60β80k','80β100k','100k+'],
|
| 41 |
+
datasets:[{label:'Avg Price (βΉL)',data:[18.2,15.4,12.8,10.6,8.3,5.9],backgroundColor:blue}]
|
| 42 |
+
}, options:{plugins:{legend:{display:false}}}
|
| 43 |
+
});
|
| 44 |
+
// Brand distribution
|
| 45 |
+
new Chart(document.getElementById('brandChart'), {
|
| 46 |
+
type:'doughnut', data:{
|
| 47 |
+
labels:['Maruti','Hyundai','Honda','Toyota','Ford','Others'],
|
| 48 |
+
datasets:[{data:[28,18,12,10,8,24],backgroundColor:[blue,'#3B82F6','#60A5FA','#93C5FD',lblue,muted]}]
|
| 49 |
+
}, options:{plugins:{legend:{position:'bottom'}}}
|
| 50 |
+
});
|
| 51 |
+
</script>
|
| 52 |
+
{% endblock %}
|
templates/index.html
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Home β PriceMyCar{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<!-- HERO -->
|
| 5 |
+
<section class="hero">
|
| 6 |
+
<div class="hero-text">
|
| 7 |
+
<div class="hero-badge">β‘ Powered by Advanced Machine Learning</div>
|
| 8 |
+
<h1>Predict Your Car<br>Price <span>Instantly</span></h1>
|
| 9 |
+
<p>Stop guessing. Get highly accurate, data-driven valuations for any used car in seconds using our state-of-the-art prediction model trained on millions of market records.</p>
|
| 10 |
+
<div class="hero-btns">
|
| 11 |
+
<a href="{{ url_for('predict_page') }}" class="btn-primary">Start Prediction β</a>
|
| 12 |
+
<a href="{{ url_for('model_info') }}" class="btn-outline">How It Works</a>
|
| 13 |
+
</div>
|
| 14 |
+
</div>
|
| 15 |
+
<div class="hero-img">
|
| 16 |
+
<svg viewBox="0 0 340 200" xmlns="http://www.w3.org/2000/svg">
|
| 17 |
+
<ellipse cx="170" cy="170" rx="160" ry="20" fill="#EFF6FF" opacity=".6"/>
|
| 18 |
+
<!-- car body -->
|
| 19 |
+
<path d="M40 130 Q60 90 120 80 L220 80 Q280 90 300 130 Z" fill="#fff" stroke="#CBD5E1" stroke-width="2"/>
|
| 20 |
+
<path d="M110 80 Q130 50 170 45 Q210 50 230 80 Z" fill="#EFF6FF" stroke="#CBD5E1" stroke-width="1.5"/>
|
| 21 |
+
<!-- windows -->
|
| 22 |
+
<path d="M120 79 Q138 56 170 52 Q202 56 220 79 Z" fill="#BFDBFE" opacity=".7"/>
|
| 23 |
+
<!-- wheels -->
|
| 24 |
+
<circle cx="100" cy="138" r="22" fill="#1E293B"/><circle cx="100" cy="138" r="12" fill="#94A3B8"/>
|
| 25 |
+
<circle cx="240" cy="138" r="22" fill="#1E293B"/><circle cx="240" cy="138" r="12" fill="#94A3B8"/>
|
| 26 |
+
<!-- red accent stripe -->
|
| 27 |
+
<path d="M60 118 Q170 112 280 118" stroke="#EF4444" stroke-width="3" fill="none" stroke-linecap="round"/>
|
| 28 |
+
</svg>
|
| 29 |
+
</div>
|
| 30 |
+
</section>
|
| 31 |
+
|
| 32 |
+
<!-- WHY -->
|
| 33 |
+
<section class="why-section">
|
| 34 |
+
<h2 class="section-heading">Why Choose PriceMyCar?</h2>
|
| 35 |
+
<p class="section-sub">Our platform combines massive datasets with sophisticated algorithms to give you the edge in the used car market.</p>
|
| 36 |
+
<div class="cards-grid">
|
| 37 |
+
<div class="card"><div class="card-icon">π―</div><h3>Accurate Prediction</h3><p>Our Random Forest model achieves into accuracy, outperforming traditional dealership appraisal methods.</p></div>
|
| 38 |
+
<div class="card"><div class="card-icon">π</div><h3>Data-Driven Analysis</h3><p>Trained on over 4,300 historical car sales, factoring in depreciation, seasonality, and regional trends.</p></div>
|
| 39 |
+
<div class="card"><div class="card-icon">β‘</div><h3>Easy to Use</h3><p>No complex spreadsheets. Just enter basic vehicle details and get an instant, understandable valuation.</p></div>
|
| 40 |
+
</div>
|
| 41 |
+
</section>
|
| 42 |
+
|
| 43 |
+
<!-- HOW IT WORKS -->
|
| 44 |
+
<section class="how-section">
|
| 45 |
+
<h2 class="section-heading">How It Works</h2>
|
| 46 |
+
<p class="section-sub">Get your car's value in three simple steps.</p>
|
| 47 |
+
<div class="steps-grid">
|
| 48 |
+
<div class="step"><div class="step-num">1</div><h3>Input Car Data</h3><p>Provide basic details like brand, model, year, mileage, and condition.</p></div>
|
| 49 |
+
<div class="step"><div class="step-num">2</div><h3>Model Processes Data</h3><p>Our ML pipeline cleans the data and runs it through our trained prediction model.</p></div>
|
| 50 |
+
<div class="step"><div class="step-num">3</div><h3>Get Price Estimation</h3><p>Receive an accurate price range along with confidence metrics and market insights.</p></div>
|
| 51 |
+
</div>
|
| 52 |
+
<a href="{{ url_for('predict_page') }}" class="btn-primary" style="margin-top:40px;display:inline-block">Try It Now</a>
|
| 53 |
+
</section>
|
| 54 |
+
{% endblock %}
|
templates/model_info.html
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Model Info - PriceMyCar{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<section class="model-page">
|
| 5 |
+
<h1>How It Works</h1>
|
| 6 |
+
<p class="section-sub">Learn how our predictive AI analyzes vehicle data to estimate the best market price.</p>
|
| 7 |
+
|
| 8 |
+
<h2 style="margin-top:40px;font-size:1.1rem;font-weight:600">Prediction Pipeline</h2>
|
| 9 |
+
<div class="pipeline">
|
| 10 |
+
<div class="pipe-step"><div class="pipe-icon">π</div><h4>1. Input Data</h4><p>You provide details like brand, model, year, and transmission.</p></div>
|
| 11 |
+
<div class="pipe-step"><div class="pipe-icon">π</div><h4>2. Condition Analysis</h4><p>The system calculates deductions based on 10 physical factors.</p></div>
|
| 12 |
+
<div class="pipe-step active"><div class="pipe-icon" style="color:#fff">βοΈ</div><h4>3. AI Valuation</h4><p>Our predictive AI matches your inputs with real-world market patterns.</p></div>
|
| 13 |
+
<div class="pipe-step"><div class="pipe-icon">π΅</div><h4>4. Price Estimation</h4><p>The app generates a price estimate and a detailed deviation analysis.</p></div>
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<div class="algo-metrics">
|
| 17 |
+
<div class="algo-section">
|
| 18 |
+
<h2>AI Prediction System</h2>
|
| 19 |
+
<p>PriceMyCar uses **Predictive AI technology** that acts as a digital professional car appraiser. This AI has learned from thousands of real-world used car sales transactions.</p>
|
| 20 |
+
<p>Unlike rigid mathematical formulas, our AI recognizes complex depreciation trendsβfor example, how the value of a luxury vehicle drops much faster than an economy family car as mileage increases.</p>
|
| 21 |
+
<h3 style="margin-top:20px;font-size:.9rem;font-weight:600">Key Parameters Evaluated by AI:</h3>
|
| 22 |
+
<ul class="feature-list">
|
| 23 |
+
<li><strong>Brand & Model:</strong> Analyzes the market popularity and demand of your specific car model.</li>
|
| 24 |
+
<li><strong>Year of Production:</strong> Evaluates natural annual depreciation based on the car's age.</li>
|
| 25 |
+
<li><strong>Mileage (Odometer):</strong> Measures physical wear and tear from vehicle usage.</li>
|
| 26 |
+
<li><strong>Fuel & Transmission:</strong> Compares market preference for automatic vs. manual transmissions and fuel types.</li>
|
| 27 |
+
<li><strong>Ownership History:</strong> Assesses the impact of past owners (first owner, second owner, etc.).</li>
|
| 28 |
+
<li><strong>Usage Frequency:</strong> Identifies whether the vehicle was rarely driven or heavily used.</li>
|
| 29 |
+
<li><strong>Physical Condition Score:</strong> Integrates the 10-factor physical evaluation (body, interior, engine, tires, etc.).</li>
|
| 30 |
+
</ul>
|
| 31 |
+
</div>
|
| 32 |
+
<div class="metrics-section">
|
| 33 |
+
<h2>AI Valuation Metrics</h2>
|
| 34 |
+
<div class="metric-row">
|
| 35 |
+
<div><div class="metric-name">AI Valuation Accuracy</div><div class="metric-desc">How accurately the AI reads and predicts key used car price factors based on real market data.</div></div>
|
| 36 |
+
<div class="metric-val">92%+</div>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="metric-row">
|
| 39 |
+
<div><div class="metric-name">Average Price Deviation</div><div class="metric-desc">The typical difference between the AI's estimate and the actual transaction price in the market.</div></div>
|
| 40 |
+
<div class="metric-val" style="color:var(--green)">~Rp 25 Million</div>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="metric-row">
|
| 43 |
+
<div><div class="metric-name">Estimation Consistency</div><div class="metric-desc">Confidence in pricing. Shows that the AI is stable and rarely produces extreme errors for most car models.</div></div>
|
| 44 |
+
<div class="metric-val" style="color:var(--green)">Very High</div>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
</section>
|
| 49 |
+
{% endblock %}
|
templates/predict.html
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Predict Car Price β PriceMyCar{% endblock %}
|
| 3 |
+
|
| 4 |
+
{% block content %}
|
| 5 |
+
<section class="predict-page">
|
| 6 |
+
<div class="predict-container">
|
| 7 |
+
|
| 8 |
+
<!-- LEFT: form -->
|
| 9 |
+
<div class="predict-form-wrap">
|
| 10 |
+
<h1>Vehicle Details</h1>
|
| 11 |
+
<p class="page-sub">Enter the specifications of the car to get an accurate price prediction.</p>
|
| 12 |
+
|
| 13 |
+
{% if error %}
|
| 14 |
+
<div class="alert alert-error">{{ error }}</div>
|
| 15 |
+
{% endif %}
|
| 16 |
+
|
| 17 |
+
<form method="POST" action="{{ url_for('predict_submit') }}" id="predictForm">
|
| 18 |
+
|
| 19 |
+
<!-- ββ Make & Model βββββββββββββββββββββββββ -->
|
| 20 |
+
<div class="form-section">
|
| 21 |
+
<div class="section-title"><span class="section-icon">π</span> Make & Model</div>
|
| 22 |
+
<div class="form-row two-col">
|
| 23 |
+
<div class="form-group">
|
| 24 |
+
<label>Brand</label>
|
| 25 |
+
<input type="text" name="brand" placeholder="e.g. Toyota" required>
|
| 26 |
+
</div>
|
| 27 |
+
<div class="form-group">
|
| 28 |
+
<label>Model</label>
|
| 29 |
+
<input type="text" name="model" placeholder="e.g. Camry, Civic, F-150" required>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<!-- ββ Age & Mileage βββββββββββββββββββββββ -->
|
| 35 |
+
<div class="form-section">
|
| 36 |
+
<div class="section-title"><span class="section-icon">π
</span> Age & Mileage</div>
|
| 37 |
+
<div class="form-row two-col">
|
| 38 |
+
<div class="form-group">
|
| 39 |
+
<label>Year of Production</label>
|
| 40 |
+
<input type="number" name="year" placeholder="e.g. 2019" min="1990" max="2025" required>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="form-group">
|
| 43 |
+
<label>Mileage (KM)</label>
|
| 44 |
+
<input type="number" name="mileage" placeholder="e.g. 45000" min="0" required>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<!-- ββ Specs βββββββββββββββββββββββββββββββ -->
|
| 50 |
+
<div class="form-section">
|
| 51 |
+
<div class="section-title"><span class="section-icon">βοΈ</span> Specifications</div>
|
| 52 |
+
<div class="form-row two-col">
|
| 53 |
+
<div class="form-group">
|
| 54 |
+
<label>Fuel Type</label>
|
| 55 |
+
<select name="fuel_type" required>
|
| 56 |
+
<option value="">Select Fuel Type</option>
|
| 57 |
+
<option>Petrol</option>
|
| 58 |
+
<option>Diesel</option>
|
| 59 |
+
<option>CNG</option>
|
| 60 |
+
<option>LPG</option>
|
| 61 |
+
<option>Electric</option>
|
| 62 |
+
</select>
|
| 63 |
+
</div>
|
| 64 |
+
<div class="form-group">
|
| 65 |
+
<label>Transmission</label>
|
| 66 |
+
<select name="transmission" required>
|
| 67 |
+
<option value="">Select Transmission</option>
|
| 68 |
+
<option>Manual</option>
|
| 69 |
+
<option>Automatic</option>
|
| 70 |
+
</select>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
<div class="form-row two-col">
|
| 74 |
+
<div class="form-group">
|
| 75 |
+
<label>Seller Type</label>
|
| 76 |
+
<select name="seller_type">
|
| 77 |
+
<option>Individual</option>
|
| 78 |
+
<option>Dealer</option>
|
| 79 |
+
<option>Trustmark Dealer</option>
|
| 80 |
+
</select>
|
| 81 |
+
</div>
|
| 82 |
+
<div class="form-group">
|
| 83 |
+
<label>Owner</label>
|
| 84 |
+
<select name="owner">
|
| 85 |
+
<option>First Owner</option>
|
| 86 |
+
<option>Second Owner</option>
|
| 87 |
+
<option>Third Owner</option>
|
| 88 |
+
<option>Fourth & Above Owner</option>
|
| 89 |
+
<option>Test Drive Car</option>
|
| 90 |
+
</select>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<!-- ββ CONDITION FACTORS (Professor's Suggestion) ββ -->
|
| 96 |
+
<div class="form-section condition-section">
|
| 97 |
+
<div class="section-title">
|
| 98 |
+
<span class="section-icon">π</span>
|
| 99 |
+
Physical Condition Factors
|
| 100 |
+
<span class="badge-new">New</span>
|
| 101 |
+
</div>
|
| 102 |
+
<p class="section-desc">
|
| 103 |
+
These factors are <strong>not in the original dataset</strong> but significantly affect real-world resale value.
|
| 104 |
+
Our scoring system applies deductions to the ML base price based on your inputs.
|
| 105 |
+
</p>
|
| 106 |
+
|
| 107 |
+
<!-- Body Damage -->
|
| 108 |
+
<div class="form-row two-col">
|
| 109 |
+
<div class="form-group">
|
| 110 |
+
<label>Body Damage Severity</label>
|
| 111 |
+
<select name="body_damage_severity">
|
| 112 |
+
<option value="0">No Damage (0%)</option>
|
| 113 |
+
<option value="1">Minor Scratches / Scuffs (-4%)</option>
|
| 114 |
+
<option value="2">Moderate Dents (-12%)</option>
|
| 115 |
+
<option value="3">Severe / Structural Damage (-28%)</option>
|
| 116 |
+
</select>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="form-group">
|
| 119 |
+
<label>Number of Dents</label>
|
| 120 |
+
<input type="number" name="dent_count" placeholder="0" min="0" max="20" value="0">
|
| 121 |
+
<small>Each dent = -2%, max -15%</small>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<!-- Paint & Interior -->
|
| 126 |
+
<div class="form-row two-col">
|
| 127 |
+
<div class="form-group">
|
| 128 |
+
<label>Paint Condition</label>
|
| 129 |
+
<select name="paint_condition">
|
| 130 |
+
<option value="excellent">Excellent (0%)</option>
|
| 131 |
+
<option value="good" selected>Good (-2%)</option>
|
| 132 |
+
<option value="fair">Fair / Faded (-6%)</option>
|
| 133 |
+
<option value="poor">Poor / Peeling (-13%)</option>
|
| 134 |
+
</select>
|
| 135 |
+
</div>
|
| 136 |
+
<div class="form-group">
|
| 137 |
+
<label>Interior Condition</label>
|
| 138 |
+
<select name="interior_condition">
|
| 139 |
+
<option value="excellent">Excellent (0%)</option>
|
| 140 |
+
<option value="good" selected>Good (-2%)</option>
|
| 141 |
+
<option value="fair">Fair / Stained (-7%)</option>
|
| 142 |
+
<option value="poor">Poor / Torn (-15%)</option>
|
| 143 |
+
</select>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<!-- Accident & Flood -->
|
| 148 |
+
<div class="form-row two-col">
|
| 149 |
+
<div class="form-group">
|
| 150 |
+
<label>Accident History</label>
|
| 151 |
+
<select name="accident_history">
|
| 152 |
+
<option value="none">No Accident (0%)</option>
|
| 153 |
+
<option value="minor">Minor Accident, Repaired (-8%)</option>
|
| 154 |
+
<option value="moderate">Moderate / Airbag Deployed (-20%)</option>
|
| 155 |
+
<option value="major">Major Accident / Total-Loss (-40%)</option>
|
| 156 |
+
</select>
|
| 157 |
+
</div>
|
| 158 |
+
<div class="form-group">
|
| 159 |
+
<label>Flood / Water Damage</label>
|
| 160 |
+
<select name="flood_damage">
|
| 161 |
+
<option value="none">None (0%)</option>
|
| 162 |
+
<option value="minor">Minor / Interior Only (-20%)</option>
|
| 163 |
+
<option value="severe">Severe / Engine Affected (-50%)</option>
|
| 164 |
+
</select>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<!-- Engine & Tires -->
|
| 169 |
+
<div class="form-row two-col">
|
| 170 |
+
<div class="form-group">
|
| 171 |
+
<label>Engine & Mechanical</label>
|
| 172 |
+
<select name="engine_condition">
|
| 173 |
+
<option value="excellent">Excellent (0%)</option>
|
| 174 |
+
<option value="good" selected>Good (-3%)</option>
|
| 175 |
+
<option value="fair">Fair / Minor Issues (-10%)</option>
|
| 176 |
+
<option value="poor">Poor / Major Repair (-30%)</option>
|
| 177 |
+
</select>
|
| 178 |
+
</div>
|
| 179 |
+
<div class="form-group">
|
| 180 |
+
<label>Tire Condition</label>
|
| 181 |
+
<select name="tire_condition">
|
| 182 |
+
<option value="good" selected>Good (>50% Tread) (0%)</option>
|
| 183 |
+
<option value="worn">Worn (20β50% Tread) (-3%)</option>
|
| 184 |
+
<option value="bald">Bald / Needs Replacement (-5%)</option>
|
| 185 |
+
</select>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<!-- Service History & Mods -->
|
| 190 |
+
<div class="form-row two-col">
|
| 191 |
+
<div class="form-group">
|
| 192 |
+
<label>Service / Maintenance History</label>
|
| 193 |
+
<select name="service_history">
|
| 194 |
+
<option value="complete">Complete Records (+3% Bonus)</option>
|
| 195 |
+
<option value="partial" selected>Partial Records (0%)</option>
|
| 196 |
+
<option value="none">No Records (-6%)</option>
|
| 197 |
+
</select>
|
| 198 |
+
</div>
|
| 199 |
+
<div class="form-group">
|
| 200 |
+
<label>Modification Status</label>
|
| 201 |
+
<select name="modification_status">
|
| 202 |
+
<option value="stock" selected>Stock / Unmodified (0%)</option>
|
| 203 |
+
<option value="cosmetic_minor">Minor Cosmetic Mods (-2%)</option>
|
| 204 |
+
<option value="cosmetic_major">Major Cosmetic Mods (-5%)</option>
|
| 205 |
+
<option value="performance">Performance Mods (-4%)</option>
|
| 206 |
+
<option value="non_reversible">Non-Reversible Mods (-8%)</option>
|
| 207 |
+
</select>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
<!-- Live penalty preview -->
|
| 212 |
+
<div class="penalty-preview" id="penaltyPreview">
|
| 213 |
+
<div class="penalty-bar-wrap">
|
| 214 |
+
<span>Estimated Condition Deduction:</span>
|
| 215 |
+
<span class="penalty-pct" id="penaltyPct">~5%</span>
|
| 216 |
+
</div>
|
| 217 |
+
<div class="penalty-bar"><div class="penalty-fill" id="penaltyFill" style="width:5%"></div></div>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
|
| 221 |
+
<button type="submit" class="btn-predict">Predict Price β</button>
|
| 222 |
+
</form>
|
| 223 |
+
</div>
|
| 224 |
+
|
| 225 |
+
<!-- RIGHT: progress sidebar -->
|
| 226 |
+
<aside class="predict-sidebar">
|
| 227 |
+
<div class="sidebar-card">
|
| 228 |
+
<h3>Data Completeness</h3>
|
| 229 |
+
<div class="progress-wrap">
|
| 230 |
+
<div class="progress-bar"><div class="progress-fill" id="progressFill" style="width:14%"></div></div>
|
| 231 |
+
<span id="progressPct">14%</span>
|
| 232 |
+
</div>
|
| 233 |
+
<ul class="completeness-list" id="completenessList">
|
| 234 |
+
<li class="pending" id="cl-make">Make & Model</li>
|
| 235 |
+
<li class="pending" id="cl-age">Age & Mileage</li>
|
| 236 |
+
<li class="pending" id="cl-spec">Specifications</li>
|
| 237 |
+
<li class="pending" id="cl-cond">Condition Factors</li>
|
| 238 |
+
</ul>
|
| 239 |
+
</div>
|
| 240 |
+
|
| 241 |
+
<div class="sidebar-card models-card">
|
| 242 |
+
<h3>π Fully Supported Models</h3>
|
| 243 |
+
<p style="font-size: 0.75rem; margin-bottom: 10px; color: var(--muted);">Use the following models for the best prediction accuracy:</p>
|
| 244 |
+
<details class="models-details">
|
| 245 |
+
<summary>View Supported Models</summary>
|
| 246 |
+
<div class="supported-models-list">
|
| 247 |
+
<strong>Toyota / Daihatsu:</strong> Avanza, Xenia, Calya, Agya, Ayla, Sigra, Rush, Terios, Yaris, Sirion, Fortuner, Innova, Corolla, Vios.<br>
|
| 248 |
+
<strong style="display:inline-block; margin-top:4px;">Honda:</strong> Brio, Jazz, Mobilio, HR-V, CR-V, Civic, City.<br>
|
| 249 |
+
<strong style="display:inline-block; margin-top:4px;">Suzuki:</strong> Ertiga, Swift, Baleno, Ignis.<br>
|
| 250 |
+
<strong style="display:inline-block; margin-top:4px;">Mitsubishi:</strong> Xpander, Pajero Sport, Mirage.<br>
|
| 251 |
+
<strong style="display:inline-block; margin-top:4px;">Nissan:</strong> Grand Livina, March.<br>
|
| 252 |
+
<strong style="display:inline-block; margin-top:4px;">Luxury Brands:</strong> Mercedes-Benz (E-Class, C-Class, M-Class), BMW (3 Series, 5 Series, 7 Series, X-Series), Audi (A4, A6, A8, Q3, Q5), Jaguar, Land Rover.
|
| 253 |
+
</div>
|
| 254 |
+
</details>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
<div class="sidebar-card how-card">
|
| 258 |
+
<h3>π‘ How it works</h3>
|
| 259 |
+
<p>Our model operates over 4,300 real-world car sales, calculating price anomalies specific to the make and model.</p>
|
| 260 |
+
<p style="margin-top:8px">The <strong>condition score</strong> then adjusts the base ML price using empirical depreciation weights for each physical factor.</p>
|
| 261 |
+
</div>
|
| 262 |
+
</aside>
|
| 263 |
+
|
| 264 |
+
</div>
|
| 265 |
+
</section>
|
| 266 |
+
{% endblock %}
|
| 267 |
+
|
| 268 |
+
{% block extra_js %}
|
| 269 |
+
<script src="{{ url_for('static', filename='js/predict.js') }}"></script>
|
| 270 |
+
{% endblock %}
|
templates/result.html
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Prediction Result - PriceMyCar{% endblock %}
|
| 3 |
+
|
| 4 |
+
{% block content %}
|
| 5 |
+
<section class="result-page">
|
| 6 |
+
<div class="result-container">
|
| 7 |
+
<a href="{{ url_for('predict_page') }}" class="back-link">β Back</a>
|
| 8 |
+
<h1>Prediction Result</h1>
|
| 9 |
+
<p class="page-sub">{{ result.inputs.year }} {{ result.inputs.brand_model }}</p>
|
| 10 |
+
|
| 11 |
+
{% if not result.is_model_supported %}
|
| 12 |
+
<div class="alert alert-warning">
|
| 13 |
+
<div class="alert-icon">β οΈ</div>
|
| 14 |
+
<div class="alert-content">
|
| 15 |
+
<h4>Model Not Fully Supported (Limited Accuracy)</h4>
|
| 16 |
+
<p>The car model <strong>{{ result.inputs.brand_model }}</strong> is not registered in our primary database. This estimate is based on the closest available segment approximation, and the actual price may vary.</p>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
{% endif %}
|
| 20 |
+
|
| 21 |
+
<div class="result-grid">
|
| 22 |
+
|
| 23 |
+
<!-- LEFT: price card + market comparison + analysis -->
|
| 24 |
+
<div class="result-left">
|
| 25 |
+
<div class="price-card">
|
| 26 |
+
<div class="confidence-badge">β‘ Powered by {{ result.ai_model }} Β· Accuracy: {{ result.accuracy_r2 }}</div>
|
| 27 |
+
<div class="price-label">Estimated Market Value</div>
|
| 28 |
+
<div class="price-main">Rp {{ "{:,.0f}".format(result.adjusted_price) }}</div>
|
| 29 |
+
|
| 30 |
+
{% if result.condition.total_penalty_pct > 0 %}
|
| 31 |
+
<div class="price-adjustment">
|
| 32 |
+
<span class="base-label">Base ML Price: Rp {{ "{:,.0f}".format(result.base_price) }}</span>
|
| 33 |
+
<span class="penalty-tag">β{{ result.condition.total_penalty_pct }}% condition</span>
|
| 34 |
+
</div>
|
| 35 |
+
{% endif %}
|
| 36 |
+
|
| 37 |
+
<div class="price-range">
|
| 38 |
+
Expected range: Rp {{ "{:,.0f}".format(result.ci_low) }} - Rp {{ "{:,.0f}".format(result.ci_high) }}
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<!-- Market Comparison bar chart (static demo) -->
|
| 43 |
+
<div class="chart-card">
|
| 44 |
+
<h3>Market Comparison</h3>
|
| 45 |
+
<div class="bar-chart">
|
| 46 |
+
<div class="bar-group">
|
| 47 |
+
<div class="bar" style="height:{{ [55,60,65,70]|random }}%" title="Trade-in"></div>
|
| 48 |
+
<span>Trade-in</span>
|
| 49 |
+
</div>
|
| 50 |
+
<div class="bar-group">
|
| 51 |
+
<div class="bar highlight" style="height:75%"></div>
|
| 52 |
+
<span>Predicted (Ours)</span>
|
| 53 |
+
</div>
|
| 54 |
+
<div class="bar-group">
|
| 55 |
+
<div class="bar" style="height:{{ [80,85,90,95]|random }}%" title="Dealer Retail"></div>
|
| 56 |
+
<span>Dealer Retail</span>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<!-- Detailed Price Deviation & Indonesian Market Analysis -->
|
| 62 |
+
<div class="analysis-card">
|
| 63 |
+
<h3>π Price Deviation & Indonesian Market Analysis</h3>
|
| 64 |
+
|
| 65 |
+
<div class="analysis-section">
|
| 66 |
+
<h4>1. Market Adjustments (June 2026)</h4>
|
| 67 |
+
<p>The base machine learning model trained on Indian data was converted using the exchange rate of <strong>1 INR = Rp 187.6</strong> and adjusted to the local Indonesian used car market with a <strong>{{ result.market_multiplier }}x</strong> multiplier (accounting for luxury tax/PPnBM, import tariffs, and 2026 inflation). This pricing is cross-referenced with major Indonesian automotive portals: <strong>OLX Indonesia</strong>, <strong>Mobil123</strong>, and <strong>GridOto Pricelist</strong>.</p>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<div class="analysis-section">
|
| 71 |
+
<h4>2. Why Real-World Prices May Deviate (Errors)</h4>
|
| 72 |
+
<p>The Machine Learning model estimates an objective valuation based on specifications, but actual transaction prices can vary due to these external real-world factors:</p>
|
| 73 |
+
|
| 74 |
+
<div class="deviation-grid">
|
| 75 |
+
<div class="dev-item">
|
| 76 |
+
<span class="dev-icon">π</span>
|
| 77 |
+
<strong>Documents & Tax Status:</strong>
|
| 78 |
+
<p>Complete paperwork (STNK & BPKB) is essential. Unpaid annual road taxes or missing documents can discount the car's value by the tax debt plus administrative fines.</p>
|
| 79 |
+
</div>
|
| 80 |
+
<div class="dev-item">
|
| 81 |
+
<span class="dev-icon">π¨</span>
|
| 82 |
+
<strong>Color Popularity:</strong>
|
| 83 |
+
<p>Neutral colors (White, Black, Silver) have high market liquidity and sell for 5-10% more than bright colors (Red, Green, Orange) in the Indonesian used car market.</p>
|
| 84 |
+
</div>
|
| 85 |
+
<div class="dev-item">
|
| 86 |
+
<span class="dev-icon">π§</span>
|
| 87 |
+
<strong>Non-Standard Modifications:</strong>
|
| 88 |
+
<p>Heavy custom modifications (engine tuning, structural body changes) narrow the buyer pool, often reducing the car's resale value compared to a stock vehicle.</p>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="dev-item">
|
| 91 |
+
<span class="dev-icon">π</span>
|
| 92 |
+
<strong>Geographical Location:</strong>
|
| 93 |
+
<p>Used car prices in the Greater Jakarta area (Jabodetabek) are highly competitive. Prices in regions outside Java can be 10-25% higher due to new vehicle distribution costs.</p>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<!-- RIGHT: key factors + condition breakdown -->
|
| 101 |
+
<div class="result-right">
|
| 102 |
+
|
| 103 |
+
<div class="factors-card">
|
| 104 |
+
<h3>π Key Factors</h3>
|
| 105 |
+
|
| 106 |
+
<div class="factor positive">
|
| 107 |
+
<div class="factor-label">Positive Impact</div>
|
| 108 |
+
<p>
|
| 109 |
+
{% if result.inputs.km < 60000 %}Low mileage ({{ "{:,}".format(result.inputs.km) }} KM) adds value.{% endif %}
|
| 110 |
+
{% if result.inputs.owner == 'First Owner' %}First-owner history is a strong positive signal.{% endif %}
|
| 111 |
+
{% if result.inputs.transmission == 'Automatic' %}Automatic transmission commands a premium.{% endif %}
|
| 112 |
+
</p>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div class="factor negative">
|
| 116 |
+
<div class="factor-label">Negative Impact</div>
|
| 117 |
+
<p>
|
| 118 |
+
{% if result.condition.total_penalty_pct > 0 %}
|
| 119 |
+
Physical condition deductions total β{{ result.condition.total_penalty_pct }}%.
|
| 120 |
+
{% endif %}
|
| 121 |
+
{% if result.inputs.km > 80000 %}
|
| 122 |
+
High mileage ({{ "{:,}".format(result.inputs.km) }} KM) reduces value.
|
| 123 |
+
{% endif %}
|
| 124 |
+
</p>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<!-- Condition Breakdown -->
|
| 129 |
+
{% if result.condition.total_penalty_pct > 0 %}
|
| 130 |
+
<div class="factors-card">
|
| 131 |
+
<h3>π Condition Breakdown</h3>
|
| 132 |
+
<table class="breakdown-table">
|
| 133 |
+
<thead><tr><th>Factor</th><th>Status</th><th>Deduction</th></tr></thead>
|
| 134 |
+
<tbody>
|
| 135 |
+
{% for key, val in result.condition.breakdown.items() %}
|
| 136 |
+
{% if val.penalty_pct != 0 %}
|
| 137 |
+
<tr>
|
| 138 |
+
<td>{{ key.replace('_', ' ').title() }}</td>
|
| 139 |
+
<td>{{ val.label }}</td>
|
| 140 |
+
<td class="{{ 'penalty-neg' if val.penalty_pct > 0 else 'penalty-pos' }}">
|
| 141 |
+
{{ '-' if val.penalty_pct > 0 else '+' }}{{ val.penalty_pct|abs }}%
|
| 142 |
+
</td>
|
| 143 |
+
</tr>
|
| 144 |
+
{% endif %}
|
| 145 |
+
{% endfor %}
|
| 146 |
+
<tr class="total-row">
|
| 147 |
+
<td colspan="2"><strong>Total Condition Adjustment</strong></td>
|
| 148 |
+
<td><strong>β{{ result.condition.total_penalty_pct }}%</strong></td>
|
| 149 |
+
</tr>
|
| 150 |
+
</tbody>
|
| 151 |
+
</table>
|
| 152 |
+
</div>
|
| 153 |
+
{% endif %}
|
| 154 |
+
|
| 155 |
+
<div class="factors-card">
|
| 156 |
+
<div class="factor-label">Market Trend</div>
|
| 157 |
+
<p>Demand for {{ result.inputs.brand_model.split()[0] }} vehicles is currently stable in the used car market.</p>
|
| 158 |
+
<a href="{{ url_for('predict_page') }}" class="btn-primary" style="margin-top:16px;display:inline-block">
|
| 159 |
+
Predict Another Car
|
| 160 |
+
</a>
|
| 161 |
+
</div>
|
| 162 |
+
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
</section>
|
| 167 |
+
{% endblock %}
|