README prêt à pusher sur GitHub — contenu en français, organisé et avec tous les blocs de code Python en triple backticks.
Copie-colle directement ce fichier dansREADME.md.
- Vue d’ensemble du workflow
- Collecte de données — stratégie concrète
- Champs (features) à extraire
- Exemple de scraping (BeautifulSoup)
- Nettoyage & préparation (pandas)
- Feature engineering
- Modélisation — pipeline exemple (scikit-learn)
- Évaluation — métriques & validation
- Problèmes récurrents & solutions
- Séparer les jeux de données
- Déploiement & livraison
- Plan de travail (sprint simple)
- Regex utiles pour parser
- Bonnes pratiques & recommandations finales
- Définir l’objectif précis — prédire le prix en TND d’une annonce voiture (occasion / neuve).
- Collecte de données (scraping) — extraire annonces depuis 4–6 sites prioritaires (ex :
automobile.tn,sayarti.tn,argusautomobile.tn,tayara.tn,sparkauto.tn,auto-plus.tn). - Nettoyage & enrichissement — normaliser prix, km, année, convertir texte en champs.
- Analyse exploratoire (EDA) — distributions, corrélations, outliers.
- Feature engineering — âge = année actuelle − année immat,
log(price), groupements, interactions. - Modeling — baselines (Linear/Tree), modèles robustes (RandomForest, XGBoost/CatBoost/LightGBM).
- Validation & métriques — MAE, RMSE, R²; cross-validation.
- Déploiement — prototype Streamlit / API (Flask/FastAPI) + Docker.
- Documentation & limites — biais, prix manquants, fiabilité des annonces.
- Choisir 4–6 sites (parmi tes 12) avec structure stable et annonces nombreuses. Priorité :
automobile.tn,sayarti.tn,tayara.tn,sparkauto.tn,auto-plus.tn,argusautomobile.tn. - Échantillonnage : prototype → ~1000–3000 annonces ; modèle solide → ≥5000 annonces.
- Stratégie d’échantillonnage : si site très grand, prendre N annonces par marque / par page (ex. 5–20).
- Électriques : extraire tous les électriques (dataset plus petit) et les traiter séparément ou ajouter
is_electric. - Respect & éthique : vérifier
robots.txt, conditions d’utilisation ; throttle requests (sleep 1–3s), user-agent, pagination soignée. - Problèmes à surveiller : annonces sans prix, prix absurdes (ex. "27 malyoum"), prix en devise différente, annonces dupliquées, données dans description (parse requis).
price(TND) — ciblebrand(Marque)model(Modèle)year(Année mise en circulation) →age = 2025 - year(ou année actuelle)mileage(Kilométrage en km)fuel(Carburant : Essence, Diesel, Electrique, Hybride)transmission(Boite : Auto/Manuelle)body(Carrosserie : berline, SUV, utilitaire, bus, ambulance)power(Puissance fiscale / réelle si dispo)engine_cc(Cylindrée)doors,seatscolor_ext,color_int(si dispo)seller_type(particulier, pro, concessionnaire) — souvent dans descriptioncondition(neuf/occasion/reconditionné)location(ville/region)images_count,has_warranty,features_list(clim, gps…) — convertir en flagsis_electric(flag)posting_age(temps depuis publication) si dispo- Traçabilité : garder
source_site,scrape_date,ad_id
Remarque : adapter les sélecteurs HTML par site. Tester toujours sur 1 page.
import requests
from bs4 import BeautifulSoup
import time
import re
import pandas as pd
HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; CarPriceBot/1.0)"}
def parse_price(text):
# normalize text like "27 000 DT" -> 27000
if not text:
return None
t = text.replace('\xa0', ' ').replace(',', '').lower()
m = re.search(r'(\d[\d\s]*)\s*(dt|tnd|dinars|dinar)?', t)
if m:
return int(m.group(1).replace(' ', ''))
return None
def parse_mileage(text):
if not text:
return None
t = text.lower().replace(' ', '')
m = re.search(r'(\d[\d,\.]*)\s*(km|kilom)', t)
if m:
return int(m.group(1).replace(',', '').replace('.', ''))
return None
def scrape_listing_page(url):
r = requests.get(url, headers=HEADERS, timeout=15)
soup = BeautifulSoup(r.text, 'html.parser')
results = []
# exemple générique : boucle sur les cartes d'annonce
for card in soup.select('.ad-card, .listing-item'):
title = card.select_one('.title, .ad-title')
price_el = card.select_one('.price')
link_el = card.select_one('a')
link = link_el['href'] if link_el else None
price = parse_price(price_el.get_text() if price_el else None)
title_text = title.get_text(strip=True) if title else ''
results.append({'title': title_text, 'price': price, 'link': link})
return results
def scrape_ad_details(ad_url):
r = requests.get(ad_url, headers=HEADERS, timeout=15)
soup = BeautifulSoup(r.text, 'html.parser')
# Exemples : adapter sélecteurs
title = soup.select_one('h1').get_text(strip=True) if soup.select_one('h1') else ''
price = parse_price(soup.select_one('.price').get_text() if soup.select_one('.price') else None)
description = soup.select_one('.description').get_text(separator=' ', strip=True) if soup.select_one('.description') else ''
# parsing simple des caractéristiques
specs = {}
for row in soup.select('.specs tr'):
cols = row.select('td')
if len(cols) >= 2:
key = cols[0].get_text(strip=True).lower()
val = cols[1].get_text(strip=True)
specs[key] = val
return {'title': title, 'price': price, 'description': description, 'specs': specs}
# Exemple d'utilisation
if __name__ == '__main__':
page_url = 'https://www.example.tn/voitures/page-1'
listings = scrape_listing_page(page_url)
df_rows = []
for l in listings[:20]:
if not l['link']:
continue
details = scrape_ad_details(l['link'])
df_rows.append(details)
time.sleep(1.5) # throttle
df = pd.DataFrame(df_rows)
print(df.head())- Normaliser prix : retirer annonces sans prix (ou garder
price_missingflag). - Convertir texte → numériques :
mileage,engine_cc,year. - Dates : convertir
year→age. - Gérer missing : imputer par médiane ou utiliser modèles qui gèrent NA (CatBoost).
- Outliers : couper prix
< 500 TNDou> 5 000 000 TNDselon contexte ; log-transform du prix souvent utile. - Feature textuelle : extraire
seller_typepar regex (mots-clés : particulier, professionnel, concessionnaire, 1ere main).
import numpy as np
df['price'] = df['price'].astype(float)
df = df[df['price'].notna()] # ou garder mais marquer
df['year'] = df['specs'].apply(lambda s: int(s.get('année', 0)) if s and s.get('année') else np.nan)
df['age'] = 2025 - df['year']
df['mileage'] = df['specs'].apply(lambda s: parse_mileage(s.get('kilométrage', '')) if s else np.nan)
# fuel doit être une colonne extraite (ex: specs.get('carburant'))
df['is_electric'] = df['fuel'].str.contains('elect', case=False, na=False).astype(int)
df['log_price'] = np.log1p(df['price'])age(mieux que rawyear)log_pricecomme cible pour réduire skewmileage_per_year = mileage / max(1, age)brand_popularity= fréquence d’apparition par marque
Encodages :
OneHotpour faible cardinalitéTarget Encoding/ embeddings pourmodelsi cardinalité haute
Autres features :
has_images/images_count— annonces avec images valent souvent plusseller_is_dealerflagis_electricséparé ou interactionbrand * is_electric
- Baselines :
LinearRegression,Ridge,Lasso - Arbres :
RandomForestRegressor,GradientBoostingRegressor - Modèles rapides et performants :
XGBoost,LightGBM,CatBoost(CatBoost gère natif les catégoriques & NA)
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.ensemble import RandomForestRegressor
import numpy as np
num_cols = ['age','mileage','engine_cc','power']
cat_cols = ['brand','model','fuel','transmission','body','seller_type']
preproc = ColumnTransformer([
('num', StandardScaler(), num_cols),
('cat', OneHotEncoder(handle_unknown='ignore', sparse=False), cat_cols),
])
pipe = Pipeline([
('pre', preproc),
('model', RandomForestRegressor(n_estimators=200, random_state=42, n_jobs=-1))
])
X = df[num_cols + cat_cols]
y = df['price'] # or 'log_price'
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
pipe.fit(X_train, y_train)
print("Test R2:", pipe.score(X_test, y_test))Contenu prêt à coller dans ton
README.md. Tous les blocs de code Python sont en triple backticks.
- MAE (Mean Absolute Error) — interprétable en TND (erreur moyenne).
- RMSE — pénalise fortement les gros écarts.
- R² — proportion de variance expliquée par le modèle.
Remarques :
- Si tu entraînes sur
log(price), reporte les métriques sur l’échelle réelle en reconvertissant :pred_real = np.expm1(pred_log). - Validation recommandée : Cross-validation (k=5 ou k=10) + hold-out final (20%) pour estimation finale.
Analyses à produire :
- Courbe residuals vs fitted (détecter biais non-linéaires).
- Erreurs moyennes par
brand, paryearet parsegment(citadine/SUV/etc.). - Distribution des erreurs (boxplots) pour détecter outliers.
- Prix manquant : supprimer les annonces sans prix ou marquer
price_missingpuis imputer si nécessaire. - Prix erronés (typos) : heuristiques (ex.
price < 1000ouprice > median * 10) + vérification manuelle d’un échantillon. - Données réparties entre pages / formats différents : architecture de scrapers modulaires (
one scraper per site), logs d’erreurs, tests unitaires pour selecteurs. - Duplicatas : déduplication via clé
(title, price, km, year)et/ou hash des images. - Électriques : si échantillon petit → entraîner modèle séparé ; sinon ajouter flag
is_electric. - Véhicules spéciaux (bus, ambulances) : exclure si l’objectif est voitures particulières.
Séparer ou tagger les subsets suivants pour entraînements et analyses :
- Électriques vs thermiques
- Occasion vs neuf
- Particulier vs professionnel
- Segments : citadine / berline / SUV / utilitaire / luxe
Ceci permet de comparer performances et comportement des modèles selon sous-populations.
- Prototype UI : Streamlit — rapide à monter (inputs → prédiction).
- API : FastAPI (recommandé) ou Flask pour exposer un endpoint
/predict(POST JSON). - Docker : Dockerfile pour containeriser l’application (API ou Streamlit).
- Monitoring : journalisation des appels et des features, stockage des prédictions vs ventes réelles pour réentraînement et dérive du modèle.
Optionnel mais pratique pour organiser le travail.
- Semaine 1 : Choix sites + inspection HTML + implémenter 1er scraper (2 sites) → collecter ~500 annonces.
- Semaine 2 : Nettoyage des données, EDA, features de base, baseline linéaire.
- Semaine 3 : Modèles avancés (XGBoost / CatBoost / LightGBM), hyperparam tuning.
- Semaine 4 : Prototype Streamlit / API FastAPI + Docker + documentation + rapport final.
Prix
m = re.search(r'(\d[\d\s]*)\s*(dt|tnd|dinar)', text, re.I)
if m:
price = int(m.group(1).replace(' ', ''))import re
m = re.search(r'(\d{4})', text)
if m:
year = int(m.group(1)) # vérifier 1980 <= year <= 2025import re
m = re.search(r'(\d[\d.,]*)\s*(km|kilom)', text, re.I)
if m:
km = int(m.group(1).replace('.', '').replace(',', ''))desc = description.lower()
if '1ère main' in desc or 'premiere main' in desc or 'première main' in desc:
seller_type = 'first_owner'
elif 'concessionnaire' in desc or 'garantie' in desc:
seller_type = 'dealer'
else:
seller_type = 'private'- Démarrer petit : 500–1000 annonces pour prototypage.
- Modularité : un scraper par site + fonctions utilitaires partagées.
- Traçabilité : stocker
source_site,scrape_date,ad_id, éventuellementraw_html. - Tests automatisés : assertions sur les selecteurs, tests d’intégration pour scrapers.
- Respect légal / éthique : vérifier
robots.txt, éviter le scraping agressif. - Features externes : envisager taux de change, indices économiques, coût moyen local pour améliorer le modèle.
car-price-prediction/
├─ data/
│ ├─ raw/ # raw html / raw JSON
│ ├─ interim/
│ └─ processed/ # cleaned CSV / parquet
├─ notebooks/
│ └─ 01_EDA_and_modeling.ipynb
├─ src/
│ ├─ scraping/
│ │ ├─ __init__.py
│ │ ├─ automobile_tn.py
│ │ ├─ sayarti_tn.py
│ │ └─ utils.py
│ ├─ preprocessing/
│ │ └─ clean.py
│ ├─ features/
│ │ └─ build_features.py
│ ├─ models/
│ │ ├─ train.py
│ │ └─ predict.py
│ └─ api/
│ └─ app.py # FastAPI or Streamlit app
├─ models/
│ └─ model.pkl
├─ Dockerfile
├─ requirements.txt
└─ README.md