Architettura del progetto
1. Layoutβ
ecom-customer-product-clustering/
βββ README.md Quick-start, traccia PW, risultati.
βββ LICENSE MIT.
βββ pyproject.toml Build config + dipendenze + entry point CLI.
βββ requirements.txt Lock approssimativo (alternativa pip install -e).
βββ mkdocs.yml Config sito documentazione.
β
βββ src/ecom_clustering/ Codice sorgente (installabile via `pip install -e .`)
β βββ __init__.py
β βββ config.py Path, costanti dominio, iperparametri, PipelineConfig.
β βββ data.py Caricamento 5 CSV + `filter_events_until` (barriera anti-leakage).
β βββ rfm.py `compute_rfm` + gestione cold users.
β βββ features.py Feature engineering esteso (16 feature utente, 10 prodotto).
β βββ clustering.py KMeans, GMM, `select_k` via silhouette, `compare_kmeans_vs_gmm`.
β βββ labeling.py `make_future_user_clusters` β costruzione label future (anti-leakage).
β βββ supervised.py RandomForest + XGBoost, `evaluate_classifier`, CV macro-F1.
β βββ evaluation.py Plot diagnostici (cluster size, profile heatmap, confmat, importance).
β βββ inference.py `predict_user_cluster` + `predict_future_cluster_supervised`.
β βββ pipeline.py Orchestratore end-to-end + CLI `ecom-cluster`.
β
βββ notebooks/ Documentazione esecutiva (4 notebook).
β βββ 01_eda_rfm.ipynb
β βββ 02_feature_engineering.ipynb
β βββ 03_user_clustering.ipynb
β βββ 04_supervised_future_cluster.ipynb
β
βββ data/ Dataset.
β βββ raw/ 5 CSV originali (~30 MB, COMMITTATI).
β β βββ events.csv 500k eventi (timestamp, user, product, action, price, ...).
β β βββ users.csv 1200 utenti.
β β βββ products.csv 3500 prodotti.
β β βββ categories.csv 15 categorie.
β β βββ promotions_daily.csv Sconti giornalieri per categoria.
β βββ processed/ Output preprocessing (gitignored).
β
βββ reports/
β βββ figures/ PNG dei plot diagnostici.
β βββ models/ Modelli serializzati (.joblib) β gitignored.
β βββ metrics.json Output metriche pipeline.
β
βββ scripts/
β βββ build_notebooks.py Genera i 4 notebook da sorgente Python.
β
βββ tests/ Smoke test (placeholder).
β
βββ docs/
β βββ index.md Home del sito.
β βββ teoria/ 5 file Markdown didattici.
β βββ scelte_tecniche/ Documenti di design (questo file + scelte-modello.md).
β βββ stylesheets/extra.css Custom styling Material.
β
βββ .github/workflows/docs.yml GitHub Actions per build + deploy GitHub Pages.
βββ venv/ Virtual environment (gitignored).
2. Principi di designβ
2.1 Separazione src/ vs notebooks/β
Il codice riutilizzabile vive in src/ecom_clustering/. I notebook sono solo presentazione: importazioni, chiamate alle funzioni di src/, narrazione didattica.
Vantaggi:
- Modifiche al codice riflesse automaticamente nei notebook (basta riavviare il kernel).
- Niente duplicazione: la logica esiste una sola volta.
- Test unitari possibili sul codice in
src/(i notebook non sono test-friendly). - Diff Git puliti su
src/(i notebook hanno output cells e metadata che inquinano i diff).
2.2 Notebook generati da scriptβ
I 4 notebook sono prodotti da scripts/build_notebooks.py. Per modificare un notebook si edita lo script e si rilancia. Vantaggi:
- RiproducibilitΓ : chiunque puΓ² rigenerare i notebook identici.
- Sorgente in formato testuale: lo script
.pyè searchable/grep-abile. - Diff puliti: niente cambi spuri di metadata o kernel hash.
2.3 Two-snapshot temporaleβ
Il PW richiede esplicitamente split temporali e validazione su periodi futuri. Il design "two-snapshot" Γ¨ la traduzione architetturale:
as_of_train = t_max β 2H: snapshot training del classificatore (feature β label at_max β H).as_of_test = t_max β H: snapshot test (feature β label at_max).
Con = future_horizon_days (default 30 giorni). Implementato in pipeline._pick_snapshots. Vedi docs/teoria/03-split-temporali-no-leakage.md per i dettagli.
2.4 Configurazione centralizzataβ
Tutti i path, le costanti dominio, le grid di iperparametri vivono in config.py. Niente magic number sparsi.
PipelineConfig Γ¨ un @dataclass(frozen=True): ogni esperimento crea un proprio config immutabile, nessuna mutazione accidentale durante la pipeline.
@dataclass(frozen=True)
class PipelineConfig:
random_state: int = 42
recent_window_days: int = 14
future_horizon_days: int = 30
kmeans_k_user: int | None = None # None β autoscelta silhouette
kmeans_k_product: int | None = None
n_jobs: int = -1
verbose: int = 1
2.5 Persistenza esplicita scaler + modelloβ
Invece di usare sklearn.pipeline.Pipeline, salviamo scaler e cluster model separati in un dizionario serializzato:
joblib.dump({
"cluster_model": cluster_user.model,
"scaler": user_scaler,
"feature_columns": user_cols,
"k": best_k_user,
"as_of": str(as_of_train),
}, user_pipeline_path)
Ragione: per il labeling futuro (make_future_user_clusters) abbiamo bisogno di accedere a scaler e modello separatamente (lo scaler standardizza nuove feature, il modello le classifica). Una Pipeline sklearn renderebbe il flusso piΓΉ opaco.
Trade-off: meno "elegante" della Pipeline, ma piΓΉ chiaro nel contesto temporale del PW.
3. ResponsabilitΓ dei moduliβ
| Modulo | ResponsabilitΓ | Dipendenze interne |
|---|---|---|
config | Path, costanti, iperparametri | nessuna |
data | I/O CSV, filtri temporali | config |
rfm | Calcolo RFM base + cold users | config |
features | Feature engineering esteso | config, rfm |
clustering | KMeans, GMM, selezione K | config |
labeling | Costruzione label future no-leakage | config, data, features |
supervised | Classificatori RF + XGB, metriche | config |
evaluation | Plot diagnostici | nessuna interna |
inference | API inferenza utente cluster | config |
pipeline | Orchestratore end-to-end | tutti gli altri |
Grafico delle dipendenze (semplificato):
config ββ¬ββ data βββ¬ββ features βββ¬ββ pipeline
β β β
βββ rfm βββ β
β β
βββ clustering βββββββββββββ€
β β
βββ supervised βββββββββββββ€
β β
βββ inference β
β
evaluation βββββββββββββββββ
labeling βββββββββββββββββββ
Tutte le frecce vanno verso pipeline. Nessuna dipendenza ciclica.
4. Punti di estensioneβ
4.1 Aggiungere una feature derivataβ
Modificare compute_user_features in features.py (o compute_product_features). Aggiungere il nome della feature in select_user_modeling_features per includerla nel clustering.
Esempio: aggiungere avg_session_length:
session_lengths = (
events_history.groupby([USER_ID_COL, "session_id"])[EVENT_TIME_COL]
.agg(lambda s: (s.max() - s.min()).total_seconds())
)
avg_session = session_lengths.groupby(level=0).mean().rename("avg_session_seconds")
rfm = rfm.join(avg_session, how="left").fillna({"avg_session_seconds": 0.0})
E poi in select_user_modeling_features:
return [
"recency_days", ..., "avg_session_seconds", # β aggiunta
]
4.2 Aggiungere un nuovo algoritmo di clusteringβ
Implementare una funzione fit_<nome> in clustering.py con la stessa firma di fit_kmeans:
def fit_dbscan(X: np.ndarray, eps: float, random_state: int = RANDOM_STATE) -> ClusteringResult:
model = DBSCAN(eps=eps, min_samples=5)
labels = model.fit_predict(X)
sil = _sample_silhouette(X, labels) if len(set(labels)) > 1 else float("nan")
return ClusteringResult(name=f"DBSCAN(eps={eps})", model=model, k=len(set(labels)),
silhouette=sil, inertia=None, labels=labels)
Per usarlo nel select_k, basta passarlo come fit_fn. Nota: DBSCAN non ha predict(), quindi non si puΓ² usare per generare label future β sarebbe solo un EDA.
4.3 Aggiungere un nuovo classificatoreβ
Implementare fit_<nome> in supervised.py con stessa firma di fit_random_forest. Aggiungere il caso in pipeline.run_full_pipeline nella sezione "Train classifier".
4.4 Cambiare l'orizzonte temporaleβ
CLI: ecom-cluster --horizon 14 (default 30).
In codice: PipelineConfig(future_horizon_days=14).
4.5 Cambiare i CSV in inputβ
Modificare le costanti in config.py:
EVENTS_FILE: str = "events.csv" # nome file
EVENT_TIME_COL: str = "timestamp" # colonna timestamp
USER_ID_COL: str = "user_id"
...
5. CLIβ
L'unico entry point CLI definito in pyproject.toml Γ¨:
[project.scripts]
ecom-cluster = "ecom_clustering.pipeline:main"
Argomenti supportati:
| Flag | Default | Descrizione |
|---|---|---|
--quick | False | Smoke test (K=5 fisso, no GMM, no select_k) |
--horizon N | 30 | Orizzonte label futura in giorni |
Estensioni possibili (non implementate):
--k-user N/--k-product N: forzare K specifici.--as-of-train YYYY-MM-DD: snapshot esplicito invece del defaultt_max - 2H.--no-xgboost: skip XGBoost (utile in ambienti senza supporto).
6. Trade-off esplicitiβ
| Scelta | Pro | Contro |
|---|---|---|
Python 3.11+ come target | Type hints moderni, match statement | Esclude Python β€ 3.10 |
xgboost>=2.1 come dipendenza | State-of-the-art tabular | ~50 MB di binari, dipendenza C++ |
sklearn>=1.6 | API moderne, output naming uniforme | Esclude utenti su versioni vecchie |
| Notebook generati da script | Diff Git puliti, riproducibilitΓ | Editing meno comodo (no live cells) |
| Two-snapshot temporal split | No leakage by construction | Non Γ¨ una stima "media" su walk-forward |
StandardScaler (no Robust) | Semplice, default sano | Sensibile a outlier su monetary |
| K-Means + GMM (no DBSCAN/Hierarchical) | predict() per label future | Nessuna gestione esplicita outlier |
class_weight="balanced" su RF | Gestisce squilibrio | XGBoost non lo supporta nativamente |
| Persistenza dict (no Pipeline) | Accesso separato a scaler/modello | Meno elegante della Pipeline sklearn |
7. Testing strategyβ
Il progetto non implementa una test suite completa β Γ¨ didattico. Smoke test inclusi:
ecom-cluster --quickesegue tutta la pipeline in <30s con K=5 fisso e senza GMM.- Esecuzione dei 4 notebook end-to-end via
nbconvert --execute(manuale).
Per produzione si dovrebbero aggiungere:
- Unit test su
compute_rfm: cold users hannofrequency=0, recency = sentinella. - Unit test su
filter_events_until: nessun evento contimestamp >= as_of. - Property-based test (Hypothesis) su
compute_user_features:recent_to_total_ratioβ [0, 1]. - Integration test su
run_full_pipeline(quick=True): produce i 3.joblibattesi. - Snapshot test sulle metriche: dato un seed, macro-F1 deve restare entro tolleranza Β±0.02.
- Test di leakage ("shuffle test"): label permutate β macro-F1 β 1/K.