Passa al contenuto principale

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 a t_max βˆ’ H).
  • as_of_test = t_max βˆ’ H: snapshot test (feature β†’ label a t_max).

Con HH = 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​

ModuloResponsabilitΓ Dipendenze interne
configPath, costanti, iperparametrinessuna
dataI/O CSV, filtri temporaliconfig
rfmCalcolo RFM base + cold usersconfig
featuresFeature engineering estesoconfig, rfm
clusteringKMeans, GMM, selezione Kconfig
labelingCostruzione label future no-leakageconfig, data, features
supervisedClassificatori RF + XGB, metricheconfig
evaluationPlot diagnosticinessuna interna
inferenceAPI inferenza utente clusterconfig
pipelineOrchestratore end-to-endtutti 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:

FlagDefaultDescrizione
--quickFalseSmoke test (K=5 fisso, no GMM, no select_k)
--horizon N30Orizzonte 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 default t_max - 2H.
  • --no-xgboost: skip XGBoost (utile in ambienti senza supporto).

6. Trade-off espliciti​

SceltaProContro
Python 3.11+ come targetType hints moderni, match statementEsclude Python ≀ 3.10
xgboost>=2.1 come dipendenzaState-of-the-art tabular~50 MB di binari, dipendenza C++
sklearn>=1.6API moderne, output naming uniformeEsclude utenti su versioni vecchie
Notebook generati da scriptDiff Git puliti, riproducibilitΓ Editing meno comodo (no live cells)
Two-snapshot temporal splitNo leakage by constructionNon Γ¨ una stima "media" su walk-forward
StandardScaler (no Robust)Semplice, default sanoSensibile a outlier su monetary
K-Means + GMM (no DBSCAN/Hierarchical)predict() per label futureNessuna gestione esplicita outlier
class_weight="balanced" su RFGestisce squilibrioXGBoost non lo supporta nativamente
Persistenza dict (no Pipeline)Accesso separato a scaler/modelloMeno elegante della Pipeline sklearn

7. Testing strategy​

Il progetto non implementa una test suite completa β€” Γ¨ didattico. Smoke test inclusi:

  • ecom-cluster --quick esegue 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 hanno frequency=0, recency = sentinella.
  • Unit test su filter_events_until: nessun evento con timestamp >= 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 .joblib attesi.
  • 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.