#r "nuget: Microsoft.ML, 5.0.0"
Console.WriteLine("Microsoft.ML 5.0.0 charge");- Microsoft.ML, 5.0.0
Microsoft.ML 5.0.0 charge
Navigation : Index | << précédent
A la fin de ce notebook, vous saurez :
ClusteringCatalog)AverageDistance) et l’indice de Davies-BouldinLes notebooks ML-1 a ML-4 traitent l’apprentissage supervise : on dispose d’etiquettes (prix d’une maison, transaction suspecte) et l’on apprend a les predire. Le clustering est non-supervise : aucune etiquette n’est fournie, l’algorithme doit seul decouvrir une structure de groupes dans les données.
Le cas d’usage canonique est la segmentation client : etant donne l’historique d’achat de milliers de clients, identifier des groupes naturels (dormants, reguliers, VIP) sans connaitre a l’avance ces catégories. ML.NET fournit le trainer K-Means via mlContext.Clustering.Trainers.KMeans.
K-Means (MacQueen, 1967 ; Lloyd, 1957) partitionne les points en K groupes en minimisant l’inertie intra-cluster (somme des distances carrees au centroide le plus proche). C’est un problème NP-difficile en general, resolu en pratique par l’heuristique de Lloyd : initialisation K-Means++ des centroides, puis itération assigner (chaque point au centroide le plus proche) / mettre a jour (chaque centroide au barycentre de ses points) jusqu’a convergence.
Microsoft.ML 5.0.0 charge
On reference ensuite les espaces de noms ML.NET et System.Linq (pour Select sur les tableaux de distances).
Espaces de noms importes (Microsoft.ML + System.Linq)
Comme dans ML-1, on commence par créer le MLContext — le point d’entree commun a toutes les opérations ML.NET. On fixe la graine (seed: 42) pour rendre K-Means déterministe et reproductible (sinon l’initialisation K-Means++ varie d’une exécution a l’autre).
La segmentation RFM (Recency, Frequency, Monetary — Hughes, 1996) classe les clients selon trois dimensions comportementales :
| Feature | Sens | Valeur haute | Valeur basse |
|---|---|---|---|
| Recency | jours depuis le dernier achat | client dormant | client actif |
| Frequency | achats par mois | client fidele | client occasionnel |
| Monetary | depense moyenne par achat (EUR) | gros panier | petit panier |
Nous generons 90 clients synthetiques issus de 3 segments caches (30 chacun) que K-Means devra retrouver sans connaitre les etiquettes. Les segments se chevauchent legerement (bruit gaussien) pour rester non-triviaux.
Definissons d’abord les classes de données ML.NET et un utilitaire gaussien (Box-Muller) pour la generation.
public class CustomerData
{
public float Recency { get; set; } // jours depuis le dernier achat (haut = dormant)
public float Frequency { get; set; } // achats par mois
public float Monetary { get; set; } // depense moyenne par achat (EUR)
}
public class ClusterPrediction
{
[ColumnName("PredictedLabel")]
public uint PredictedClusterId { get; set; }
[ColumnName("Score")]
public float[] Distances { get; set; }
}
public static class RandUtil
{
static readonly Random _r = new Random(42);
// Box-Muller : echantillonne un flottant selon N(mean, std), clame a >= 0
public static float NextGaussian(float mean, float std)
{
double u1 = 1.0 - _r.NextDouble();
double u2 = 1.0 - _r.NextDouble();
double z = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2);
return Math.Max(0f, mean + (float)(z * std));
}
}
Console.WriteLine("Classes CustomerData / ClusterPrediction / RandUtil definies");Classes CustomerData / ClusterPrediction / RandUtil definies
On genere maintenant les trois segments. Chaque client est tire d’une gaussienne centree sur le profil du segment (ex. le VIP achete recemment, souvent, avec un gros panier). K-Means ne verra jamais l’appartenance a un segment — il doit la reconstruire depuis la seule geometrie des points.
// 3 segments de clients (segments CACHES, que K-Means doit retrouver)
var customers = new List<CustomerData>();
// Segment "Dormants" : achat ancien, peu frequent, petit panier
for (int i = 0; i < 30; i++)
customers.Add(new CustomerData {
Recency = RandUtil.NextGaussian(85, 15),
Frequency = RandUtil.NextGaussian(2, 1),
Monetary = RandUtil.NextGaussian(50, 20)
});
// Segment "Reguliers" : achat recent, frequence moyenne, panier moyen
for (int i = 0; i < 30; i++)
customers.Add(new CustomerData {
Recency = RandUtil.NextGaussian(30, 8),
Frequency = RandUtil.NextGaussian(8, 2),
Monetary = RandUtil.NextGaussian(150, 40)
});
// Segment "VIP" : achat tres recent, tres frequent, gros panier
for (int i = 0; i < 30; i++)
customers.Add(new CustomerData {
Recency = RandUtil.NextGaussian(8, 3),
Frequency = RandUtil.NextGaussian(20, 4),
Monetary = RandUtil.NextGaussian(500, 100)
});
Console.WriteLine($"Genere {customers.Count} clients : 3 segments caches x 30");Genere 90 clients : 3 segments caches x 30
On charge la liste dans un IDataView — la structure colonnaire fondamentale de ML.NET, déjà utilisee dans ML-1 et ML-2.
Le pipeline comporte trois étapes. La concatenation regroupe les trois features en un vecteur "Features". La normalisation MinMax ramene chaque feature dans [0, 1]. Le trainer K-Means partitionne ensuite l’espace normalise en numberOfClusters: 3 groupes.
Pourquoi normaliser absolument ? K-Means mesure la proximite par distance euclidienne. Sans normalisation, Monetary (50 a 500 EUR) ecrase totalement Frequency (2 a 20) et Recency (8 a 85 jours) : la distance serait dominee par la depense, et le clustering reflete surtout l’argent. Normaliser met les trois dimensions a la même echelle — c’est l’objet de l’Exercice 3. C’est l’equivalent, en non-supervise, de l’ingenierie de features vue en ML-2.
var pipeline = mlContext.Transforms
.Concatenate("Features", nameof(CustomerData.Recency), nameof(CustomerData.Frequency), nameof(CustomerData.Monetary))
.Append(mlContext.Transforms.NormalizeMinMax("Features"))
.Append(mlContext.Clustering.Trainers.KMeans("Features", numberOfClusters: 3));
Console.WriteLine("Pipeline cree : Concatenate -> NormalizeMinMax -> KMeans(K=3)");Pipeline cree : Concatenate -> NormalizeMinMax -> KMeans(K=3)
On entraine le modèle en appelant Fit sur l’IDataView, exactement comme en ML-1.
L’evaluation d’un clustering est delicate : contrairement a la classification (ML-4), il n’y a pas d’etiquette de verite a laquelle comparer. On mesure donc la qualite intrinseque de la partition :
AverageDistance (inertie intra-cluster) : distance moyenne de chaque point au centroide de son cluster. Plus bas = groupes compacts. C’est la grandeur que K-Means minimise.DaviesBouldinIndex (Davies & Bouldin, 1979) : rapport moyen entre la dispersion intra-cluster et la separation inter-cluster. Plus bas = clusters a la fois compacts et bien separes.On obtient ces metriques via mlContext.Clustering.Evaluate, qui calcule les distances a partir de la colonne "Features" (normalisee).
var predictions = model.Transform(dataView);
var metrics = mlContext.Clustering.Evaluate(predictions, featureColumnName: "Features");
Console.WriteLine("=== Metriques de clustering (K=3) ===");
Console.WriteLine($" AverageDistance (inertie intra-cluster) : {metrics.AverageDistance:F4}");
Console.WriteLine($" DaviesBouldinIndex (separation) : {metrics.DaviesBouldinIndex:F3}");=== Metriques de clustering (K=3) ===
AverageDistance (inertie intra-cluster) : 0,0209
DaviesBouldinIndex (separation) : 0,394
| Metrique | Valeur | Lecture |
|---|---|---|
| AverageDistance | 0,0209 | compacite elevee (features normalisees dans [0,1]) |
| DaviesBouldinIndex | 0,394 | separation nette (< 0,5 = clusters bien separes) |
L’indice de Davies-Bouldin de 0,394 est nettement sous le seuil de 0,5 : les trois clusters sont a la fois compacts et bien separes. Au-dela de 1,0 on parlerait de clusters qui se chevauchent. Les trois segments RFM etant volontairement bien distingues (Dormants / Reguliers / VIP), K-Means a effectivement retrouve la structure cachee – sans jamais avoir vu les etiquettes de segment.
On créé un PredictionEngine (comme en ML-1) pour inferer le cluster de nouveaux clients. La prediction renvoie :
PredictedClusterId : le cluster assigne (1 a K),Distances : le vecteur des distances (normalisees) aux K centroides — utile pour mesurer la certitude (un point a mi-chemin de deux centroides est ambigus).var engine = mlContext.Model.CreatePredictionEngine<CustomerData, ClusterPrediction>(model);
CustomerData[] testClients = new[] {
new CustomerData { Recency = 90, Frequency = 1, Monetary = 40 }, // profil dormant
new CustomerData { Recency = 25, Frequency = 9, Monetary = 160 }, // profil regulier
new CustomerData { Recency = 5, Frequency = 22, Monetary = 520 }, // profil VIP
};
Console.WriteLine("=== Prediction du segment de nouveaux clients ===");
foreach (var c in testClients)
{
var p = engine.Predict(c);
string distStr = string.Join(", ", p.Distances.Select(d => d.ToString("F2")));
Console.WriteLine($" Recency={c.Recency} Freq={c.Frequency} Monetary={c.Monetary}EUR -> Cluster {p.PredictedClusterId} (distances normalisees: [{distStr}])");
}=== Prediction du segment de nouveaux clients ===
Recency=90 Freq=1 Monetary=40EUR -> Cluster 2 (distances normalisees: [0,45, 0,01, 1,50])
Recency=25 Freq=9 Monetary=160EUR -> Cluster 1 (distances normalisees: [0,00, 0,43, 0,39])
Recency=5 Freq=22 Monetary=520EUR -> Cluster 3 (distances normalisees: [0,56, 1,52, 0,01])
Les trois profils tombent dans trois clusters distincts, et pour chacun la distance a son cluster est negligeable devant les deux autres :
| Profil | Cluster assigne | Distances (normalisees) aux 3 centroides |
|---|---|---|
| dormant (R90 F1 M40EUR) | 2 | [0,45 ; 0,01 ; 1,50] |
| regulier (R25 F9 M160EUR) | 1 | [0,00 ; 0,43 ; 0,39] |
| VIP (R5 F22 M520EUR) | 3 | [0,56 ; 1,52 ; 0,01] |
La separation est nette : chaque client est quasi exactement sur un centroide (distance ~0,00-0,01) et loin des deux autres. K-Means a donc reconstruit, sans aucune etiquette, la segmentation Dormants / Reguliers / VIP. Les numéros de cluster (1, 2, 3) sont arbitraires – ils dependent de l’initialisation K-Means++ – mais la partition est correcte.
Mais comment sait-on que K=3 est le bon nombre de clusters ? C’est la question centrale du clustering non-supervise. La méthode du coude (Thorndike, 1953) consiste a entrainer K-Means pour plusieurs valeurs de K et a observer l’evolution de l’inertie (AverageDistance) :
On complete avec l’indice de Davies-Bouldin : son minimum pointe vers le K optimal. Si les deux critères convergent vers K=3, c’est un signal fort que les données ont structurellement 3 groupes.
Console.WriteLine("=== Choix de K : methode du coude (K=2..6) ===");
Console.WriteLine(" K | AverageDistance | DaviesBouldin");
Console.WriteLine(" --+-----------------+---------------");
for (int k = 2; k <= 6; k++)
{
var sweepPipe = mlContext.Transforms
.Concatenate("Features", nameof(CustomerData.Recency), nameof(CustomerData.Frequency), nameof(CustomerData.Monetary))
.Append(mlContext.Transforms.NormalizeMinMax("Features"))
.Append(mlContext.Clustering.Trainers.KMeans("Features", numberOfClusters: k));
var sweepModel = sweepPipe.Fit(dataView);
var sweepMetrics = mlContext.Clustering.Evaluate(sweepModel.Transform(dataView), featureColumnName: "Features");
Console.WriteLine($" {k} | {sweepMetrics.AverageDistance,15:F4} | {sweepMetrics.DaviesBouldinIndex,13:F3}");
}=== Choix de K : methode du coude (K=2..6) ===
K | AverageDistance | DaviesBouldin
--+-----------------+---------------
2 | 0,0961 | 0,513
3 | 0,0209 | 0,394
4 | 0,0190 | 0,759
5 | 0,0133 | 0,875
6 | 0,0115 | 0,901
| K | AverageDistance (inertie) | DaviesBouldin |
|---|---|---|
| 2 | 0,0961 | 0,513 |
| 3 | 0,0209 | 0,394 |
| 4 | 0,0190 | 0,759 |
| 5 | 0,0133 | 0,875 |
| 6 | 0,0115 | 0,901 |
Lecture : l’inertie chute brutalement entre K=2 (0,096) et K=3 (0,021) – une baisse de 78 % – puis la pente s’adoucit fortement (K=4 ne gagne plus que 9 %, et la decroissance devient lineaire pour K=5 et K=6). Le coude est nettement a K=3. Parallelement, l’indice de Davies-Bouldin atteint son minimum a K=3 (0,394) puis remonte pour K superieur (les clusters supplementaires fragmentent les vrais groupes, degradant la separation). La convergence des deux critères – coude de l’inertie ET minimum du Davies-Bouldin – confirme que 3 est le nombre structurel de segments dans ces données RFM. C’est précisément le nombre de segments caches que nous avions genere.
Le jeu principal ci-dessus (3 segments Dormants / Reguliers / VIP) est presqu’unidimensionnel : la depense Monetary seule (50 / 150 / 500 EUR) separe déjà les trois segments. L’Exercice 3 le reconnait (« sans normalisation, le clustering reflete surtout la depense »). Sur un tel cas, K-Means n’apporte pas grand-chose qu’un simple seuil sur Monetary ne donnerait pas.
La capacite distinctive de K-Means — combiner plusieurs features pour separer des segments qu’aucune feature isolee ne distingue — ne s’exerce que sur un jeu ou chaque feature prise separement chevauche les segments. On construit donc trois segments disposes en triangle dans le plan (Recency, Frequency) :
Recency bas, Frequency basse,Recency haut, Frequency haute,Recency bas, Frequency haute.Les deux premières coordonnees se croisent : sur l’axe Recency seul, jeune-actif et ancien-frequent se chevauchent ; sur l’axe Frequency seul, ancien-occasionnel et ancien-frequent se chevauchent. Monetary est partage (bruit irrelatif). Aucune feature isolee ne separe les trois segments ; seul le plan (Recency, Frequency) le permet.
// === Redemption : 3 segments en TRIANGLE — chaque feature 1-D chevauche ===
// Sur le jeu principal, Monetary seul separait les segments (clustering quasi 1-D).
// Ici, les segments se croisent sur chaque axe isole : seul le plan (R,F) les separe.
var triCustomers = new List<CustomerData>();
var triLabels = new List<int>();
for (int i = 0; i < 50; i++) { triCustomers.Add(new CustomerData { Recency = RandUtil.NextGaussian(30, 9), Frequency = RandUtil.NextGaussian(30, 9), Monetary = RandUtil.NextGaussian(150, 40) }); triLabels.Add(0); } // jeune-actif
for (int i = 0; i < 50; i++) { triCustomers.Add(new CustomerData { Recency = RandUtil.NextGaussian(70, 9), Frequency = RandUtil.NextGaussian(70, 9), Monetary = RandUtil.NextGaussian(150, 40) }); triLabels.Add(1); } // ancien-occasionnel
for (int i = 0; i < 50; i++) { triCustomers.Add(new CustomerData { Recency = RandUtil.NextGaussian(30, 9), Frequency = RandUtil.NextGaussian(70, 9), Monetary = RandUtil.NextGaussian(150, 40) }); triLabels.Add(2); } // ancien-frequent
int[] triTruth = triLabels.ToArray();
// Exactitude de clustering = meilleur appariement 1-a-1 entre clusters trouves et verite (6 permutations de 3 clusters).
static float ClusterAcc(uint[] pred, int[] truth) {
var perms = new[]{ new[]{0,1,2}, new[]{0,2,1}, new[]{1,0,2}, new[]{1,2,0}, new[]{2,0,1}, new[]{2,1,0} };
int n = pred.Length, best = 0;
foreach (var pm in perms) { int c = 0; for (int i = 0; i < n; i++) if (pm[pred[i] - 1] == truth[i]) c++; best = Math.Max(best, c); }
return (float)best / n;
}
// Baseline triviale : meilleur seuil 1-D (2 coupures -> 3 bins) sur UNE feature, contre la verite.
static float Best1D(float[] feat, int[] truth) {
var perms = new[]{ new[]{0,1,2}, new[]{0,2,1}, new[]{1,0,2}, new[]{1,2,0}, new[]{2,0,1}, new[]{2,1,0} };
float best = 0; var sorted = feat.OrderBy(v => v).ToArray();
for (int ci = 0; ci < feat.Length; ci += 2) for (int cj = ci + 2; cj < feat.Length; cj += 2) {
double c1 = sorted[ci], c2 = sorted[cj]; int[] bin = new int[feat.Length];
for (int i = 0; i < feat.Length; i++) bin[i] = feat[i] <= c1 ? 0 : (feat[i] <= c2 ? 1 : 2);
foreach (var pm in perms) { int c = 0; for (int i = 0; i < feat.Length; i++) if (pm[bin[i]] == truth[i]) c++; best = Math.Max(best, (float)c / feat.Length); }
}
return best;
}
var triDv = mlContext.Data.LoadFromEnumerable(triCustomers);
var triPipe = mlContext.Transforms
.Concatenate("Features", nameof(CustomerData.Recency), nameof(CustomerData.Frequency), nameof(CustomerData.Monetary))
.Append(mlContext.Transforms.NormalizeMinMax("Features"))
.Append(mlContext.Clustering.Trainers.KMeans("Features", numberOfClusters: 3));
var triModel = triPipe.Fit(triDv);
var triPreds = triModel.Transform(triDv).GetColumn<uint>("PredictedLabel").ToArray();
float accKm = ClusterAcc(triPreds, triTruth);
float accR = Best1D(triCustomers.Select(c => c.Recency).ToArray(), triTruth);
float accF = Best1D(triCustomers.Select(c => c.Frequency).ToArray(), triTruth);
float accM = Best1D(triCustomers.Select(c => c.Monetary).ToArray(), triTruth);
float best1D = Math.Max(accR, Math.Max(accF, accM));
Console.WriteLine("=== Segments en triangle (chaque feature 1-D chevauche) ===");
Console.WriteLine($" Recency seul (meilleur seuil 1-D) : {accR:F3} (jeune-actif & ancien-frequent se chevauchent sur R)");
Console.WriteLine($" Frequency seul (meilleur seuil 1-D) : {accF:F3} (ancien-occasionnel & ancien-frequent se chevauchent sur F)");
Console.WriteLine($" Monetary seul (bruit partage) : {accM:F3} (aucun signal)");
Console.WriteLine($" Meilleure baseline 1 feature : {best1D:F3}");
Console.WriteLine($" K-Means 3D (Recency+Frequency+Mon) : {accKm:F3}");
Console.WriteLine($" Avantage K-Means multivarie : +{accKm - best1D:F3}");=== Segments en triangle (chaque feature 1-D chevauche) ===
Recency seul (meilleur seuil 1-D) : 0,720 (jeune-actif & ancien-frequent se chevauchent sur R)
Frequency seul (meilleur seuil 1-D) : 0,733 (ancien-occasionnel & ancien-frequent se chevauchent sur F)
Monetary seul (bruit partage) : 0,427 (aucun signal)
Meilleure baseline 1 feature : 0,733
K-Means 3D (Recency+Frequency+Mon) : 0,993
Avantage K-Means multivarie : +0,260
| méthode | Exactitude | Lecture |
|---|---|---|
Seuil Recency seul |
~0,70 | separe ancien-occasionnel des deux autres, mais jeune-actif et ancien-frequent se chevauchent sur R |
Seuil Frequency seul |
~0,73 | separe jeune-actif des deux autres, mais ancien-occasionnel et ancien-frequent se chevauchent sur F |
Seuil Monetary seul |
~0,42 | aucun signal (Monetary est partage) |
| K-Means 3D (R+F+M) | ~0,99 | combine les features et retrouve les 3 segments |
Aucune feature isolee ne depasse ~0,73 : chacune laisse deux segments chevauchants que seuillage 1-D ne peut separer. Pourtant K-Means atteint ~0,99 : en projetant sur les trois features simultanement, il exploite le fait que jeune-actif et ancien-frequent (qui se ressemblent sur Recency) se distinguent sur Frequency, et ancien-occasionnel et ancien-frequent (qui se ressemblent sur Frequency) se distinguent sur Recency. C’est précisément la capacite que aucun seuil par feature isolee ne peut reproduire : le clustering multivarie extrait la structure du plan (Recency, Frequency), pas celle d’un axe.
Contraste avec le jeu principal : sur Dormants / Reguliers / VIP, Monetary seul separait déjà tout (clustering quasi unidimensionnel). Ici, aucun axe ne suffit — K-Means est le seul outil qui marche. Les deux cas ensemble montrent quand K-Means est justifie : non pas sur des segments qu’un seul indicateur separe, mais sur des structures relationnelles que seule la geometrie multivariee revele.
Reutilisez la generation ci-dessus, mais avec 4 segments caches distincts (ajoutez par exemple un segment “Nouveaux” : Recency très bas, Frequency très basse, Monetary moyen). Cherchez ensuite le K optimal.
Objectifs : 1. Generer un nouveau jeu customers4 avec 4 segments x 25 points 2. Le charger dans un IDataView 3. Boucler K=2..7, entrainer K-Means, evaluer (AverageDistance + DaviesBouldin) 4. Identifier le K avec le DaviesBouldin minimum = votre K optimal
Indice : recopiez la boucle de sweep ci-dessus sur customers4. Le coude de l’inertie et le minimum du DaviesBouldin doivent pointer vers K=4.
// Exercice 1 : Determiner le K optimal sur un dataset a 4 segments
// TODO etudiant : genere 4 clusters caches (4 x 25 points), puis cherche le K optimal (2..7)
// Indice : recopie la boucle de sweep ci-dessus sur customers4 ; le DaviesBouldin minimum
// et le coude de AverageDistance doivent pointer vers K=4.
// Etape 1 : genere un nouveau jeu customers4 avec 4 segments distincts
// Etape 2 : charge-le dans un IDataView
// Etape 3 : boucle K=2..7, entraine KMeans, evalue
// Etape 4 : affiche le K avec le DaviesBouldin minimum = ton K optimal
Console.WriteLine("Exercice 1 a completer");Exercice 1 a completer
La silhouette est une mesure plus fine que le Davies-Bouldin. Pour chaque point i :
\[s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))}\]
ou a(i) est la distance moyenne de i aux autres points de son cluster, et b(i) la distance moyenne de i au cluster voisin le plus proche. La silhouette globale est la moyenne des s(i) ; elle varie dans [-1, 1] (proche de 1 = clustering excellent).
Objectifs : 1. Recuperer les features normalisees et les labels via model.Transform(dataView).GetColumn<float[]>("Features") et GetColumn<uint>("PredictedLabel") 2. Calculer la matrice des distances euclidiennes (double boucle sur les 90 points) 3. Pour chaque point, calculer a(i) puis b(i), puis s(i) 4. Moyenner et verifier que K=3 maximise la silhouette (vs K=2, K=4)
Indice : a(i) moyenne sur le cluster de i (exclure i lui-même) ; b(i) = minimum sur les autres clusters de la distance moyenne a ce cluster.
// Exercice 2 : Implementer la silhouette moyenne (Rousseeuw, 1987)
// TODO etudiant : la silhouette s(i) = (b(i) - a(i)) / max(a(i), b(i))
// a(i) = distance moyenne de i aux autres points de SON cluster
// b(i) = distance moyenne de i au cluster voisin le plus proche
// Indice : recupere les features normalisees et les labels via
// var feats = model.Transform(dataView).GetColumn<float[]>("Features").ToArray();
// var labels = model.Transform(dataView).GetColumn<uint>("PredictedLabel").ToArray();
// Etape 1 : calcule la matrice des distances euclidiennes (double boucle)
// Etape 2 : pour chaque point, calcule a(i) puis b(i)
// Etape 3 : moyenne s(i) sur tous les points = silhouette globale
// Etape 4 : verifie que K=3 maximise la silhouette (vs K=2, K=4)
Console.WriteLine("Exercice 2 a completer");Exercice 2 a completer
Le pipeline principal normalise (NormalizeMinMax). Que se passe-t-il sans normalisation ?
Objectifs : 1. Construire un pipeline sans NormalizeMinMax (Concatenate -> KMeans, K=3) 2. L’entrainer, l’evaluer (AverageDistance, DaviesBouldin) 3. Predire les 3 testClients et comparer les clusters a la version normalisee 4. Conclure : pourquoi la normalisation est-elle indispensable en clustering ?
Indice : Monetary (50 a 500 EUR) domine totalement Recency et Frequency en echelle brute. Sans normalisation, le clustering reflete surtout la depense — les clusters se forment essentiellement le long de l’axe Monetary.
// Exercice 3 : Effet de la normalisation sur K-Means
// TODO etudiant : entraine KMeans SANS NormalizeMinMax et compare les clusters trouves.
// Indice : Monetary (50..500 EUR) domine totalement Recency et Frequency en echelle brute ;
// sans normalisation, le clustering reflete surtout la depense.
// Etape 1 : construis un pipeline SANS NormalizeMinMax (Concatenate -> KMeans)
// Etape 2 : entraine, evalue (AverageDistance, DaviesBouldin)
// Etape 3 : predit les 3 testClients et compare les clusters a la version normalisee
// Etape 4 : conclus : pourquoi la normalisation est-elle indispensable en clustering ?
Console.WriteLine("Exercice 3 a completer");Exercice 3 a completer
Ce notebook a introduit le clustering non-supervise avec K-Means :
| Concept | Description |
|---|---|
| Clustering | Apprentissage non-supervise : trouver des groupes sans etiquettes |
ClusteringCatalog |
Catalogue ML.NET pour K-Means (mlContext.Clustering.Trainers.KMeans) |
| AverageDistance | Inertie intra-cluster (plus bas = groupes compacts) |
| DaviesBouldinIndex | Mesure de separation (plus bas = clusters mieux separes) |
| NormalizeMinMax | Indispensable : met les features a la même echelle avant la distance euclidienne |
| méthode du coude | Choix de K : la ou l’inertie cesse de baisser fortement |
Points cles :
Algorithme (K-Means)
Metriques d’evaluation
Cas d’usage (segmentation RFM)