Segmenter les clients d'une boutique en ligne
- Étude de cas 6
- 120 min
- Facultative · 1 étude de cas requise pour le certificat
Étude de cas 6 sur 7 · Apprentissage automatique : du problème à un modèle fiableSegmenter les clients d'une boutique en ligne
1. Comprendre l'apprentissage automatique
2. Cadrer le problème de prédiction
3. Évaluer les modèles supervisés
4. Créer les premiers modèles
5. Essayer d'autres familles de modèles
6. Régler et finaliser le modèle
7. Comprendre et choisir le modèle
8. Préparer la passation du modèle
Ce que vous saurez faire
Transformez deux ans de factures réelles d’une boutique en ligne en segments utilisables par une équipe de gestion de la relation client (CRM). Le nettoyage représente la plus grande partie du travail. Ensuite, vous regroupez les clients avec k-means et choisissez le nombre de groupes selon trois types d’indices. Vient ensuite la vérification que la plupart des tutoriels omettent : les segments ont-ils prédit les comportements des six mois suivants? À la fin, vous comparez les groupes à un score simple que tout le monde peut calculer. Vous décidez ensuite quelle méthode l’équipe devrait utiliser.
Le carnet (notebook) Jupyter contient tous les blocs de code de cette page, dans le même ordre. Il comprend trois tâches et leurs vérifications. Ses cellules de préparation installent les paquets et téléchargent les données (15 Mo). Aucune configuration locale n’est donc nécessaire. Il s’exécute en environ une minute sur un ordinateur portable. Lisez les explications ici et exécutez le code dans le carnet.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Le regroupement (clustering) et pourquoi une personne doit juger les groupes, dans L’apprentissage non supervisé.
- k-means, la mise à l’échelle avant le regroupement, l’inertie et la silhouette, dans L’exploration non supervisée.
- Les pipelines, dans Prétraiter avec un pipeline.
- Les données de test gardées à l’écart, dans Données d’entraînement, de validation et de test, et l’aire sous la courbe ROC, dans Le compromis entre précision et rappel.
- Le journal d’expériences, dans L’apprentissage automatique est itératif.
- Pourquoi une méthode plus simple peut être le meilleur choix, dans Quand un modèle simple est meilleur.
Ce qu’il vous faut sur votre ordinateur (seulement pour l’exécution locale; le carnet n’en a pas besoin)
- Un environnement Python avec pandas 3.0.6, scikit-learn 1.9.1 et matplotlib. Le
.venvde votre projet de cours, dans Définir une référence de base : préparer votre projet, les contient. Sipython -c "import sklearn"échoue, activez de nouveau l’environnement. - Un nouveau dossier vide pour cette étude de cas, par exemple
~/projects/case-studies. Ouvrez le carnet dans ce dossier. Sa cellule de préparation crée un dossiersegment-online-shop-customerset y télécharge les données. Pour recommencer, supprimez ce dossier et exécutez le carnet depuis le début.
Testé sur macOS avec Python 3.14.6, pandas 3.0.6, scikit-learn 1.9.1 et matplotlib 3.11.2, dans Jupyter. Windows, Colab et Kaggle n’ont pas été testés.
La situation
Section intitulée « La situation »Une boutique en ligne du Royaume-Uni vend des cadeaux et des articles pour la maison. Beaucoup de ses clients sont de petites boutiques qui achètent pour revendre. Un bon client commande donc régulièrement. Hannah, responsable CRM de la boutique, planifie la deuxième moitié de l’année. Elle dispose de trois moyens, chacun avec un budget fixe :
- Un service VIP pour environ 750 clients : livraison gratuite et personne-ressource attitrée. Ce service coûte cher. Il doit donc aller aux clients qui ont vraiment le plus de valeur.
- Une offre de reconquête : un bon de réduction de 10 % par courriel pour les clients devenus inactifs. Un bon envoyé à un client qui aurait acheté de toute façon réduit inutilement la marge.
- Un suivi régulier : un courriel mensuel présentant les nouveaux produits, pour tous les autres. Il coûte peu.
Chaque erreur a un coût. Le service VIP pour un client qui part quand même gaspille de l’argent. Un excellent client qui ne reçoit rien peut se sentir négligé et partir. Un bon offert à un client fidèle réduit la marge sans rien apporter. Owen, responsable des finances, pose une question pour chaque segment : « Si je dépense pour ce groupe, achètera-t-il plus que les autres? »
Hannah, Owen et leurs plans sont fictifs et servent à cette étude de cas. Les données sont réelles : ce sont les factures de la boutique, publiées pour la recherche.
Ce que vous allez créer
Section intitulée « Ce que vous allez créer »Vous allez créer une segmentation de la clientèle : une ligne par client, avec un nom de segment pour l’outil de courriel de Hannah. Vous fournissez aussi des preuves que les segments ont un sens. Un segment est un groupe de clients qui reçoivent le même traitement. Vous créez les segments à partir de trois nombres par client, le résumé classique RFM : récence, fréquence et montant des achats. Vous vérifiez les segments selon ce que les clients ont fait dans les six mois suivants. Vous les comparez aussi à un score simple.
Cette étude de cas utilise les compétences suivantes du cours :
- vérifier et nettoyer les données avant tout modèle;
- utiliser k-means après la mise à l’échelle, avec l’inertie et la silhouette (L’exploration non supervisée);
- créer un pipeline qui apprend la mise à l’échelle à partir des données (Prétraiter avec un pipeline);
- établir une référence de base à dépasser (Définir une référence de base);
- évaluer avec des données gardées à l’écart jusqu’à la fin (Données d’entraînement, de validation et de test);
- choisir entre une méthode complexe et une méthode simple (Quand un modèle simple est meilleur).
Les données
Section intitulée « Les données »Les données proviennent de Online Retail II, dans le UCI Machine Learning Repository. Elles contiennent toutes les lignes de facture de la boutique du 1 décembre 2009 au 9 décembre 2011. Daqing Chen les a publiées sous la licence Creative Commons Attribution 4.0 (CC BY 4.0). Cette licence permet leur réutilisation avec mention de la source.
Chen, D. (2012). Online Retail II [Jeu de données]. UCI Machine Learning Repository. https://doi.org/10.24432/C5CG6D. CC BY 4.0.
Le cours conserve une copie fixe dans son dépôt de travaux pratiques. La source est un fichier Excel à deux feuilles, une par année. Le cours les a réunies dans un seul fichier CSV compressé, sans rien changer d’autre. La fiche du jeu de données énumère les changements, comme l’exige la licence.
Une ligne correspond à une ligne de facture : un produit sur une facture. Voici les colonnes :
| Colonne | Signification |
|---|---|
Invoice |
Le numéro de facture. Un numéro qui commence par C indique une annulation : le client a retourné la marchandise. |
StockCode |
Le code du produit, généralement à cinq chiffres. |
Description |
Le nom du produit. |
Quantity |
Le nombre d’unités sur la ligne; négatif pour un retour. |
InvoiceDate |
La date et l’heure de la facture. |
Price |
Le prix d’une unité, en livres (£). |
Customer ID |
Le numéro du client. |
Country |
Le pays du client. |
Chargez les données. Invoice et StockCode sont lus comme du texte, car certaines valeurs contiennent des lettres.
lines = pd.read_csv("data/online_retail_ii.csv.gz", dtype={"Invoice": str, "StockCode": str}, parse_dates=["InvoiceDate"])print(f"{len(lines):,} lines, {lines.shape[1]} columns")print("from", lines["InvoiceDate"].min(), "to", lines["InvoiceDate"].max())lines.head(4)Le résultat :
1,067,371 lines, 8 columnsfrom 2009-12-01 07:45:00 to 2011-12-09 12:50:00 Invoice StockCode Description Quantity InvoiceDate Price Customer ID Country0 489434 85048 15CM CHRISTMAS GLASS BALL 20 LIGHTS 12 2009-12-01 07:45:00 6.95 13085.0 United Kingdom1 489434 79323P PINK CHERRY LIGHTS 12 2009-12-01 07:45:00 6.75 13085.0 United Kingdom2 489434 79323W WHITE CHERRY LIGHTS 12 2009-12-01 07:45:00 6.75 13085.0 United Kingdom3 489434 22041 RECORD FRAME 7" SINGLE SIZE 48 2009-12-01 07:45:00 2.10 13085.0 United KingdomCe que cela nous apprend. Il y a un peu plus d’un million de lignes, sur deux ans et neuf jours. Les quatre premières lignes appartiennent à une même facture. Le client 13 085 a acheté quatre produits à 7 h 45 le 1 décembre 2009. Customer ID s’affiche sous la forme 13085.0, un nombre décimal. Cela indique des valeurs manquantes : pandas stocke une colonne de nombres entiers avec des valeurs manquantes sous forme décimale.
Premières vérifications
Section intitulée « Premières vérifications »Avant de créer quoi que ce soit, comptez les problèmes. Chaque ligne ci-dessous pose une question.
missing_id = lines["Customer ID"].isna()returns = lines["Invoice"].str.startswith("C")print(f"no customer ID: {missing_id.sum():>8,} lines ({missing_id.mean():.1%})")print(f"exact repeated lines: {lines.duplicated().sum():>8,}")print(f"return lines (C...): {returns.sum():>8,}")print(f"quantity below 0: {(lines['Quantity'] < 0).sum():>8,}")print(f"price 0 or below: {(lines['Price'] <= 0).sum():>8,}")print(f"countries: {lines['Country'].nunique()}, United Kingdom {(lines['Country'] == 'United Kingdom').mean():.1%}")Le résultat :
no customer ID: 243,007 lines (22.8%)exact repeated lines: 34,335return lines (C...): 19,494quantity below 0: 22,950price 0 or below: 6,207countries: 43, United Kingdom 91.9%Ce que cela nous apprend. Près du quart des lignes n’ont pas de client. Un segment est un groupe de clients. Ces lignes ne peuvent donc pas servir. 34 335 lignes sont des copies exactes d’une autre ligne, dans toutes les colonnes. Il y a plus de lignes avec une quantité négative (22 950) que de lignes de retour (19 494). Certaines quantités négatives ne sont donc pas des retours. La plupart des clients sont au Royaume-Uni. Chaque problème exige une décision. Les prochaines vérifications en cherchent les causes.
Pourquoi y a-t-il autant de doublons exacts? Examinez les jours où ils apparaissent :
repeated = lines[lines.duplicated()]in_overlap = repeated["InvoiceDate"].between("2010-12-01", "2010-12-09 23:59")print(f"{len(repeated):,} repeated lines; {in_overlap.sum():,} of them are from 1 to 9 December 2010")Le résultat :
34,335 repeated lines; 22,844 of them are from 1 to 9 December 2010Ce que cela nous apprend. Les deux tiers des doublons se trouvent sur neuf jours. Le fichier source contient une feuille par année. Les deux feuilles contiennent les dates du 1 au 9 décembre 2010. Ces lignes représentent donc les mêmes ventes, enregistrées deux fois à cause de la création du fichier. Les 11 491 autres doublons se répartissent sur les deux années. Il s’agit probablement du même produit numérisé deux fois sur une facture, mais personne ne peut en être certain. Vous supprimez tous les doublons exacts et consignez ce choix.
Ensuite, les codes de produit. Un code de produit contient cinq chiffres, parfois accompagnés d’une lettre. Examinez les codes qui ne suivent pas ce format :
odd = lines[~lines["StockCode"].str.match(r"^\d{5}")]print(f"{len(odd):,} lines with a stock code that does not start with five digits")(odd.groupby("StockCode") .agg(lines=("Invoice", "size"), example=("Description", "first"), price=("Price", "median")) .sort_values("lines", ascending=False).head(12))Le résultat :
6,093 lines with a stock code that does not start with five digits lines example priceStockCodePOST 2122 POSTAGE 18.000DOT 1446 DOTCOM POSTAGE 154.180M 1421 Manual 12.680C2 282 CARRIAGE 50.000D 177 Discount 19.000S 104 SAMPLES 29.995BANK CHARGES 102 Bank Charges 42.120ADJUST 67 Adjustment by john on 26/01/2010 16 49.350AMAZONFEE 43 AMAZON FEE 6497.470DCGS0058 31 MISO PRETTY GUM 0.830gift_0001_20 29 Dotcomgiftshop Gift Voucher £20.00 17.020gift_0001_30 29 Dotcomgiftshop Gift Voucher £30.00 25.530Ce que cela nous apprend. La plupart de ces lignes ne représentent pas des marchandises. Ce sont des frais postaux, de transport, des corrections manuelles, des rabais, des frais bancaires, de plateforme et des chèques-cadeaux. Ces montants sont réels, mais ne disent rien sur ce qu’un client achète. Des frais Amazon de 6 497 £ sur une ligne feraient paraître un client riche. DCGS0058 est différent : c’est un vrai produit (de la gomme à mâcher) avec un code inhabituel. Vous ne pouvez donc pas supprimer « tous les codes qui ne sont pas à cinq chiffres ». Vous énumérez plutôt les codes qui ne représentent pas des produits, par leur nom. La fiche du jeu de données contient la liste complète.
Enfin, examinez les plus grandes quantités :
huge = lines["Quantity"].abs() > 50_000lines.loc[huge, ["Invoice", "StockCode", "Description", "Quantity", "Price", "Customer ID", "InvoiceDate"]]Le résultat :
Invoice StockCode Description Quantity Price Customer ID InvoiceDate587080 541431 23166 MEDIUM CERAMIC TOP STORAGE JAR 74215 1.04 12346.0 2011-01-18 10:01:00587085 C541433 23166 MEDIUM CERAMIC TOP STORAGE JAR -74215 1.04 12346.0 2011-01-18 10:17:001065882 581483 23843 PAPER CRAFT , LITTLE BIRDIE 80995 2.08 16446.0 2011-12-09 09:15:001065883 C581484 23843 PAPER CRAFT , LITTLE BIRDIE -80995 2.08 16446.0 2011-12-09 09:27:00Ce que cela nous apprend. Le client 12 346 a commandé 74 215 bocaux de rangement pour 77 184 £. Il a annulé la commande 16 minutes plus tard. Le client 16 446 a fait la même chose avec 80 995 articles de bricolage en papier. Ce sont des erreurs, pas des achats. Si vous supprimez les retours et gardez les commandes, le client 12 346 devient l’un des plus gros clients de la boutique. Les retours doivent donc rester dans les données. Les dépenses doivent être nettes : achats moins retours.
Nettoyer les lignes
Section intitulée « Nettoyer les lignes »Chaque problème reçoit maintenant une décision. Hannah accepte ces règles, car chacune découle de sa question : « Quels clients inscrits achètent nos marchandises, et pour quel montant? »
| Problème | Décision | Raison |
|---|---|---|
| Doublons exacts | Supprimer | La plupart viennent du chevauchement des deux feuilles; un doublon compterait une vente deux fois. |
| Aucun identifiant de client | Supprimer | Impossible de relier la ligne à un client. Hannah ne peut pas contacter un client sans identifiant. |
| Codes qui ne représentent pas des produits | Supprimer | Les frais postaux, les autres frais et les bons ne sont pas des achats de marchandises. |
| Prix de 0 ou moins | Supprimer | Articles gratuits et corrections comptables, pas des ventes. |
Retours (C…) |
Garder | Les dépenses doivent exclure les montants retournés, sinon les commandes erronées comptent comme des ventes. |
Le code consigne chaque étape. Le rapport peut ainsi indiquer combien de lignes chaque règle a supprimées :
NOT_PRODUCTS = ["POST", "DOT", "C2", "C3", "M", "m", "D", "S", "B", "BANK CHARGES", "ADJUST", "ADJUST2", "AMAZONFEE", "CRUK", "TEST001", "TEST002", "GIFT"]steps = []
def keep(df, rows_to_keep, reason): """Keep some lines, and record how many were removed and why.""" steps.append({"removed because": reason, "lines removed": int((~rows_to_keep).sum()), "lines left": int(rows_to_keep.sum())}) return df[rows_to_keep]
clean = keep(lines, ~lines.duplicated(), "exact repeat of another line")clean = keep(clean, clean["Customer ID"].notna(), "no customer ID")product = ~clean["StockCode"].isin(NOT_PRODUCTS) & ~clean["StockCode"].str.startswith("gift_")clean = keep(clean, product, "not a product (postage, fee, voucher)")clean = keep(clean, clean["Price"] > 0, "price 0 or below")pd.DataFrame(steps)Le résultat :
removed because lines removed lines left0 exact repeat of another line 34335 10330361 no customer ID 235151 7978852 not a product (postage, fee, voucher) 3641 7942443 price 0 or below 61 794183Ce que cela nous apprend. Il reste 794 183 lignes, soit 74,4 % du nombre initial. L’ordre des étapes change les comptes. Par exemple, 6 207 lignes avaient un prix de 0 ou moins. Il n’en reste que 61 à la dernière étape, car presque toutes n’avaient pas d’identifiant de client. Présentez donc les comptes dans l’ordre où vous avez appliqué les règles. Les lignes sans identifiant de client représentent la plus grande perte. Elles peuvent correspondre à des acheteurs invités qui ne se sont jamais inscrits. Les segments décrivent donc seulement les clients inscrits. Cela fait partie des limites.
Ajoutez maintenant deux colonnes : l’identifiant de client en texte et le montant de chaque ligne en livres. Vérifiez ensuite que les retours se comportent comme prévu.
clean = clean.assign(customer=clean["Customer ID"].astype(int).astype(str), amount=clean["Quantity"] * clean["Price"])returns = clean["Invoice"].str.startswith("C")print(f"{clean['customer'].nunique():,} customers, {clean['Invoice'].nunique():,} invoices")print(f"return lines: {returns.sum():,}; all with a negative quantity: {(clean.loc[returns, 'Quantity'] < 0).all()}")print(f"negative quantity but not a return: {((clean['Quantity'] < 0) & ~returns).sum()}")print("customer 12346, net amount per invoice (£):")print(clean[clean["customer"] == "12346"].groupby("Invoice")["amount"].sum().round(2).to_string())Le résultat :
5,875 customers, 43,877 invoicesreturn lines: 17,587; all with a negative quantity: Truenegative quantity but not a return: 0customer 12346, net amount per invoice (£):Invoice499763 27.05513774 142.31541431 77183.60C541433 -77183.60Ce que cela nous apprend. Les données nettoyées contiennent 5 875 clients. Chaque ligne de retour a une quantité négative, et aucune autre ligne n’en a. Les 3 456 lignes négatives qui n’étaient pas des retours (22 950 − 19 494) ont toutes été supprimées. La commande erronée du client 12 346 et son retour s’annulent exactement. Ses dépenses nettes sont donc de 27,05 £ + 142,31 £ = 169,36 £.
Cadrer le problème
Section intitulée « Cadrer le problème »Le regroupement n’a pas de cible. Les questions de Traduire un besoin changent donc un peu. Voici les réponses pour le plan de Hannah.
- L’unité est un client, car Hannah agit sur les clients, pas sur les factures.
- Le moment de décision est le jour où elle prépare le plan. Dans cette étude de cas, c’est le 1 juin 2011 : la date de référence. Tous les calculs pour un client utilisent seulement les lignes antérieures à cette date, comme si nous étions le 1 juin 2011.
- La vérification. Il n’y a pas d’étiquette avec laquelle comparer. Mais les données continuent après la date de référence. Vous pouvez donc voir ce que les clients ont réellement fait dans les six mois suivants, de juin à novembre 2011. C’est l’équivalent, pour le regroupement, des données de test gardées à l’écart. Vous créez et choisissez tout sur l’historique. Vous ouvrez les données futures une seule fois, à la fin. Les neuf jours de décembre 2011 sont exclus, pour une fenêtre d’exactement six mois.
- Les fuites. Tout nombre calculé à partir de lignes après le 1 juin introduirait le futur dans les segments. La fonction RFM ci-dessous utilise seulement l’historique.
- Les colonnes sensibles. Il n’y a ni noms, ni âges, ni adresses. Les identifiants de client sont des numéros attribués par la boutique. Vous ne devez pas essayer d’identifier qui que ce soit.
Countryn’entre pas dans les segments. Vous l’utilisez seulement pour vérifier les segments dans chaque marché.
Séparez les lignes nettoyées à la date de référence :
SNAPSHOT = pd.Timestamp("2011-06-01")FUTURE_END = pd.Timestamp("2011-12-01")history = clean[clean["InvoiceDate"] < SNAPSHOT]future = clean[(clean["InvoiceDate"] >= SNAPSHOT) & (clean["InvoiceDate"] < FUTURE_END)]for name, part in [("history", history), ("future", future)]: print(f"{name:8} {len(part):>7,} lines, {part['customer'].nunique():,} customers, " f"{part['InvoiceDate'].min():%Y-%m-%d} to {part['InvoiceDate'].max():%Y-%m-%d}")print(f"not used: {(clean['InvoiceDate'] >= FUTURE_END).sum():,} lines from 1 to 9 December 2011")Le résultat :
history 541,106 lines, 4,932 customers, 2009-12-01 to 2011-05-31future 235,767 lines, 3,489 customers, 2011-06-01 to 2011-11-30not used: 17,310 lines from 1 to 9 December 2011Ce que cela nous apprend. L’historique couvre 18 mois et 4 932 clients. Le futur couvre six mois et 3 489 clients. Certains sont de nouveaux clients, sans historique, qui ne peuvent pas encore recevoir de segment. D’ici la vérification sur les données futures, ne regardez pas future.
Récence, fréquence et valeur monétaire
Section intitulée « Récence, fréquence et valeur monétaire »Une commerçante qui connaît ses clients pose trois questions sur chacun. Quand a-t-il acheté pour la dernière fois? À quelle fréquence achète-t-il? Combien dépense-t-il? Ce sont les trois nombres RFM. Le marketing direct les utilisait bien avant l’apprentissage automatique :
- Récence : nombre de jours entre le dernier achat et la date de référence. Une petite valeur est bonne.
- Fréquence : nombre de factures distinctes (commandes), sans compter les retours.
- Valeur monétaire : dépenses nettes en livres, soit les achats moins les retours.
Un petit exemple montre comment les calculer à partir des lignes. Le client A a commandé le 10 mai (100 £) et le 20 mai (50 £). Il a retourné 20 £ de marchandises le 25 mai. À la date de référence du 1 juin, sa récence est de 12 jours (depuis le 20 mai). Sa fréquence est de 2. Sa valeur monétaire est de 100 £ + 50 £ − 20 £ = 130 £.
def rfm_table(history, snapshot): """One row per customer: recency, frequency and monetary value at the snapshot date.""" purchases = history[~history["Invoice"].str.startswith("C")] table = purchases.groupby("customer").agg(last_purchase=("InvoiceDate", "max"), frequency=("Invoice", "nunique")) table["recency"] = (snapshot - table["last_purchase"]).dt.days table["monetary"] = history.groupby("customer")["amount"].sum().round(2) # purchases minus returns return table[["recency", "frequency", "monetary"]]
rfm = rfm_table(history, SNAPSHOT)print(f"{len(rfm):,} customers with a purchase; net spend 0 or below: {(rfm['monetary'] <= 0).sum()}")rfm = rfm[rfm["monetary"] > 0]rfm.describe().round(1)Le résultat :
4,908 customers with a purchase; net spend 0 or below: 15 recency frequency monetarycount 4893.0 4893.0 4893.0mean 167.4 5.2 2287.8std 141.2 9.9 10202.8min 0.0 1.0 1.625% 43.0 1.0 308.050% 145.0 3.0 724.075% 242.0 6.0 1869.1max 546.0 234.0 383393.4Ce que cela nous apprend. Parmi les 4 932 clients de l’historique, 24 ont seulement des retours. Ils ont acheté avant le début des données et n’ont donc pas de ligne RFM. Quinze autres ont retourné autant qu’ils ont acheté. Avec des dépenses nettes de 0 ou moins, ce ne sont pas des clients à segmenter. Il reste donc 4 893 clients. Le .round(2) est important. Sans lui, un client dont les achats et les retours s’annulent peut avoir des dépenses nettes de 0,00000000000001 et passer le filtre. Comparez maintenant la moyenne et la médiane (50 %) de chaque colonne. Le client médian a dépensé 724 £, mais la moyenne est de 2 288 £. Le plus gros client a dépensé 383 393 £.
Explorer ce que la décision exige
Section intitulée « Explorer ce que la décision exige »La question de Hannah porte sur les quelques clients qui comptent le plus. La vue importante est donc celle des écarts entre les clients :
print("skew:", rfm.skew().round(1).to_dict())top_tenth = rfm["monetary"].nlargest(len(rfm) // 10).sum() / rfm["monetary"].sum()print(f"the top 10% of customers by spend made {top_tenth:.1%} of the spend")print(f"bought only once: {(rfm['frequency'] == 1).mean():.1%} of customers")Le résultat :
skew: {'recency': 0.8, 'frequency': 10.0, 'monetary': 24.2}the top 10% of customers by spend made 61.4% of the spendbought only once: 31.3% of customersCe que cela nous apprend. L’asymétrie mesure le déséquilibre d’une colonne : 0 indique une distribution équilibrée. Une grande valeur positive indique une longue traîne de très grandes valeurs. La fréquence (10,0) et les dépenses (24,2) ont de très longues traînes. Un client sur dix représente 61,4 % des dépenses, et près d’un sur trois n’a acheté qu’une fois. C’est une bonne nouvelle pour Hannah : un petit groupe mérite vraiment une attention particulière. C’est une mauvaise nouvelle pour k-means, qui utilise les distances. Un client à 383 393 £ est si loin des autres que les distances refléteraient seulement les dépenses. Les prochaines sections traitent ce problème.
Une référence de base simple : le score RFM par quartiles
Section intitulée « Une référence de base simple : le score RFM par quartiles »Avant tout regroupement, créez la méthode qu’une équipe de marketing utiliserait sans apprentissage automatique. Triez les clients selon chacun des trois nombres et divisez-les en quatre groupes égaux (quartiles). Donnez 0 point au pire quart et 3 au meilleur. Un achat récent est bon, donc la récence se trie dans l’autre sens. Le score RFM est la somme : de 0 à 9 points. C’est la même idée que le score par points dans Quand un modèle simple est meilleur. Tout le monde peut le calculer et l’expliquer.
def quartile(values): """0 for the lowest quarter of the customers, 3 for the highest.""" return pd.qcut(values.rank(method="first"), 4, labels=False)
rfm["rfm_score"] = quartile(-rfm["recency"]) + quartile(rfm["frequency"]) + quartile(rfm["monetary"])rfm["rfm_score"].value_counts().sort_index().to_frame().TLe résultat :
rfm_score 0 1 2 3 4 5 6 7 8 9count 376 528 572 537 514 513 433 418 432 570Ce que cela nous apprend. Chaque score de 0 à 9 compte de 376 à 572 clients. 570 clients ont un score de 9 : achats récents, fréquents et dépenses élevées. rank(method="first") départage les égalités. 31,3 % des clients ont une fréquence de 1. Sans cette méthode, les quarts ne pourraient donc pas être égaux. Ce score est la référence de base. Les groupes doivent être au moins aussi utiles que le score. Sinon, Hannah devrait utiliser le score.
Préparer les nombres pour k-means
Section intitulée « Préparer les nombres pour k-means »k-means regroupe les clients selon leur distance. Il faut donc d’abord changer deux choses :
- Prendre le logarithme. Le logarithme d’un nombre augmente avec son nombre de chiffres : 100 £, 1 000 £ et 10 000 £ deviennent 4,6, 6,9 et 9,2. Un client qui dépense 100 fois plus se trouve maintenant à quelques unités, plutôt qu’à un million de livres.
np.log1pcalcule log(1 + x), ce qui fonctionne même avec une récence de 0 jour. - Mettre à l’échelle. Après le logarithme, les trois colonnes ont encore des unités différentes.
StandardScalerexprime chaque colonne en écarts-types, comme dans L’exploration non supervisée.
La mise à l’échelle apprend une moyenne et un écart-type à partir des données. Les deux étapes vont donc dans un pipeline. FunctionTransformer transforme une fonction ordinaire, ici np.log1p, en étape de pipeline :
from sklearn.cluster import KMeansfrom sklearn.pipeline import make_pipelinefrom sklearn.preprocessing import FunctionTransformer, StandardScaler
RFM = ["recency", "frequency", "monetary"]prepare = make_pipeline(FunctionTransformer(np.log1p), StandardScaler())X = prepare.fit_transform(rfm[RFM])print("skew after the log:", np.log1p(rfm[RFM]).skew().round(1).to_dict())pd.DataFrame(X, columns=RFM).describe().round(2).loc[["mean", "std", "min", "max"]]Le résultat :
skew after the log: {'recency': -1.0, 'frequency': 1.1, 'monetary': 0.2} recency frequency monetarymean -0.00 0.00 -0.00std 1.00 1.00 1.00min -3.53 -1.00 -4.30max 1.35 5.34 4.66Ce que cela nous dit. Après le logarithme, l’asymétrie des dépenses passe de 24,2 à 0,2, et celle de la fréquence de 10,0 à 1,1. Après la mise à l’échelle, chaque colonne a une moyenne de 0 et un écart-type de 1. Les valeurs extrêmes sont maintenant à environ 5 écarts-types de la moyenne, plutôt qu’à des centaines. La récence penche maintenant dans l’autre sens (−1,0), car le logarithme étire les petites valeurs. C’est acceptable : aucun client ne détermine à lui seul les distances.
Choisir le nombre de segments
Section intitulée « Choisir le nombre de segments »k-means a besoin de k, le nombre de groupes, avant de commencer. L’exploration non supervisée présentait deux indices. L’inertie est la somme des distances au carré jusqu’aux centres; elle baisse toujours quand k augmente. La silhouette va de −1 à 1. Une valeur élevée indique des groupes clairs et distincts. Ici, vous ajoutez un troisième indice : la stabilité.
Hannah ne peut pas bâtir un plan sur des segments qui changent beaucoup quand les données changent peu. Ajustez donc k-means 10 fois, chaque fois sur un échantillon aléatoire différent contenant 80 % des clients. Affectez ensuite chaque client à un groupe avec chaque ajustement. Comparez les 10 résultats par paires avec l’indice de Rand ajusté (ARI). L’ARI mesure à quel point deux regroupements concordent. Une valeur de 1 signifie qu’ils placent exactement les mêmes clients ensemble; 0 indique une concordance équivalente au hasard. Les numéros des groupes ne comptent pas, seulement qui se trouve avec qui.
from sklearn.metrics import adjusted_rand_score, silhouette_score
def stability(X, k, runs=10): """Mean agreement (ARI) between k-means runs fitted on different random 80% samples.""" rng = np.random.default_rng(0) labels = [] for run in range(runs): rows = rng.choice(len(X), int(0.8 * len(X)), replace=False) labels.append(KMeans(n_clusters=k, n_init=10, random_state=run).fit(X[rows]).predict(X)) return np.mean([adjusted_rand_score(a, b) for i, a in enumerate(labels) for b in labels[i + 1:]])
for k in range(2, 9): km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X) print(f"k {k}: inertia {km.inertia_:4.0f} silhouette {silhouette_score(X, km.labels_):.3f} " f"stability {stability(X, k):.3f} sizes {sorted(np.bincount(km.labels_).tolist(), reverse=True)}")Le résultat :
k 2: inertia 7213 silhouette 0.456 stability 0.970 sizes [3155, 1738]k 3: inertia 5443 silhouette 0.349 stability 0.930 sizes [2143, 1845, 905]k 4: inertia 4209 silhouette 0.378 stability 0.964 sizes [1990, 1424, 761, 718]k 5: inertia 3552 silhouette 0.327 stability 0.866 sizes [1532, 1334, 902, 627, 498]k 6: inertia 3108 silhouette 0.328 stability 0.922 sizes [1338, 1208, 811, 577, 527, 432]k 7: inertia 2814 silhouette 0.288 stability 0.717 sizes [1103, 1001, 759, 627, 534, 509, 360]k 8: inertia 2552 silhouette 0.285 stability 0.891 sizes [1065, 966, 739, 604, 504, 432, 420, 163]Chaque exécution de la vérification de stabilité utilise aussi un autre départ aléatoire (random_state=run). La stabilité montre donc si le résultat dépend de la graine aléatoire. Voici les mêmes nombres sous forme de graphiques :
Description textuelle du graphique
Un graphique en courbe de l’inertie selon k, de 2 à 8. L’inertie passe de 7 213 à k = 2 à 5 443 à k = 3, puis à 4 209 à k = 4. Ensuite, les baisses sont plus petites : 3 552, 3 108, 2 814 et 2 552 à k = 8. Les baisses sont de 1 770 et 1 234, puis de 657, 444, 294 et 262 : le coude se trouve à k = 4.
Description textuelle du graphique
Deux courbes selon k, de 2 à 8. La silhouette est maximale à k = 2 (0,456), baisse à 0,349 à k = 3, puis remonte à 0,378 à k = 4. Elle baisse ensuite à environ 0,33 à k = 5 et 6, puis à 0,29 à k = 7 et 8. La stabilité est de 0,970 à k = 2, de 0,930 à k = 3 et de 0,964 à k = 4. Elle est de 0,866 à k = 5, de 0,922 à k = 6, de 0,717 à k = 7 et de 0,891 à k = 8.
Ce que cela nous dit. Les trois indices vont dans le même sens :
- L’inertie baisse de 1 770 et de 1 234 aux deux premières étapes, puis de seulement 657. Le changement de pente (le « coude ») se trouve à k = 4.
- La silhouette est maximale à k = 2 (0,456). Mais deux groupes donneraient seulement « actifs » et « inactifs », alors que Hannah dispose de trois outils. Parmi les valeurs plus grandes de k, k = 4 présente un maximum local (0,378). Toutes les valeurs sont inférieures à 0,5, donc les groupes se touchent. Ce sont des découpages utiles dans un même nuage, pas des îles.
- La stabilité est élevée à k = 4 (0,964) : les mêmes clients se retrouvent ensemble, peu importe les 80 % utilisés. À k = 5, elle baisse à 0,866, et à k = 7, à 0,717.
Vous choisissez donc k = 4. Les données n’ont pas décidé seules. Elles ont dit « 4 est un bon découpage ». L’entreprise a dit « 4 groupes conviennent à nos trois outils, plus un groupe qui ne reçoit presque rien ».
Ajuster les segments
Section intitulée « Ajuster les segments »Ajustez maintenant le pipeline complet avec k = 4. Examinez ensuite le profil de chaque groupe : la médiane de chaque nombre et la part des dépenses totales.
segmenter = make_pipeline(FunctionTransformer(np.log1p), StandardScaler(), KMeans(n_clusters=4, n_init=10, random_state=0))rfm["cluster"] = segmenter.fit_predict(rfm[RFM])profile = rfm.groupby("cluster").agg(customers=("recency", "size"), recency=("recency", "median"), frequency=("frequency", "median"), monetary=("monetary", "median"), spend=("monetary", "sum"))profile["share_of_spend"] = profile["spend"] / profile["spend"].sum()profile.drop(columns="spend").round(3)Le résultat :
customers recency frequency monetary share_of_spendcluster0 718 22.0 3.0 715.055 0.0561 1424 174.0 4.0 1358.085 0.2292 761 19.0 13.0 4854.140 0.6583 1990 238.5 1.0 282.585 0.057Ce que cela nous dit. Chaque groupe représente un type de client clair :
- Groupe 2, 761 clients (15,6 %) : leur dernière commande date de 19 jours avant la date de référence. Ils ont commandé 13 fois et dépensé 4 854 £ chacun. Ils représentent 65,8 % des dépenses totales.
- Groupe 0, 718 clients : ils ont acheté récemment (22 jours), mais seulement 3 fois, pour 715 £.
- Groupe 1, 1 424 clients : 4 commandes et 1 358 £, mais la dernière remonte à 174 jours, presque six mois.
- Groupe 3, 1 990 clients (40,7 %) : une commande, de 283 £, il y a 238 jours. Ils représentent seulement 5,7 % des dépenses.
Sur un autre ordinateur, les numéros des groupes peuvent être dans un autre ordre. Les groupes restent les mêmes; les nombres de 0 à 3 sont seulement des noms.
Hannah nomme les segments
Section intitulée « Hannah nomme les segments »Vous montrez le profil à Hannah. Elle connaît ces clients et donne à chaque groupe un nom qui indique quoi faire :
| Groupe | Nom donné par Hannah | Son interprétation | Son plan |
|---|---|---|---|
| 2 | Meilleurs clients | « Nos revendeurs réguliers. En perdre un nous coûte cher. » | Service VIP |
| 0 | Acheteurs occasionnels récents | « Des boutiques nouvelles ou petites qui ont acheté récemment. Elles pourraient grandir. » | Accompagnement |
| 1 | Clients réguliers inactifs | « Ils commandaient tous les quelques mois, puis ils ont arrêté. Une offre vaut la peine. » | Offre de reconquête |
| 3 | Acheteurs uniques inactifs | « Une commande, il y a longtemps. La plupart ne reviendront pas. » | Un rappel peu coûteux seulement |
Le code attribue les noms selon le profil, pas selon les numéros des groupes. Il donne donc le bon nom sur tout ordinateur :
names = {profile["frequency"].idxmax(): "Best customers", profile["recency"].idxmax(): "Lapsed one-time buyers"}rest = profile.drop(index=list(names))names[rest["recency"].idxmin()] = "Recent occasional buyers"names[rest["recency"].idxmax()] = "Quiet regulars"rfm["segment"] = rfm["cluster"].map(names)profile.index = profile.index.map(names)profile.drop(columns="spend").round(3)Le résultat :
customers recency frequency monetary share_of_spendclusterRecent occasional buyers 718 22.0 3.0 715.055 0.056Quiet regulars 1424 174.0 4.0 1358.085 0.229Best customers 761 19.0 13.0 4854.140 0.658Lapsed one-time buyers 1990 238.5 1.0 282.585 0.057Ce que cela nous dit. Ce sont les mêmes groupes qu’avant, maintenant décrits dans les mots de Hannah. Un nom est un résumé, pas une règle : « uniques » décrit la médiane. Certains acheteurs inactifs ont commandé deux ou trois fois.
Le graphique montre 500 clients (un échantillon aléatoire fixe) selon deux des trois nombres.
- Meilleurs clients
- Occasionnels récents
- Réguliers inactifs
- Uniques inactifs
Description textuelle du graphique
Un nuage de points. L’axe horizontal représente la récence, de 0 à 550 jours. L’axe vertical représente les dépenses nettes sur une échelle logarithmique, de 10 £ à 100 000 £. Les meilleurs clients (78 dans l’échantillon) sont en haut à gauche. Neuf sur dix ont une récence inférieure à 50 jours. Leurs dépenses vont de 1 600 £ à 79 000 £, surtout de 2 000 £ à 12 000 £. Les acheteurs occasionnels récents (81) sont aussi à gauche, avec une récence inférieure à 75 jours, mais plus bas. Leurs dépenses sont surtout de 270 £ à 1 500 £. Les clients réguliers inactifs (125) sont au milieu, surtout entre 70 et 260 jours, avec des dépenses de 650 £ à 3 000 £. Les acheteurs uniques inactifs (216) sont à droite et en bas. La plupart ont une récence de 85 à 460 jours, et neuf sur dix ont dépensé moins de 600 £. Les groupes se touchent; leurs frontières dépendent aussi de la fréquence, que le graphique ne montre pas.
Les meilleurs clients et les acheteurs occasionnels récents ont tous acheté récemment; les dépenses et la fréquence les séparent. Les clients réguliers inactifs et les acheteurs uniques inactifs ont tous arrêté d’acheter. Les acheteurs uniques inactifs ont dépensé moins et commandé une seule fois.
La vérification sur la période suivante : les six prochains mois
Section intitulée « La vérification sur la période suivante : les six prochains mois »Ouvrez maintenant la partie mise de côté. Pour chaque client : a-t-il acheté de nouveau entre juin et novembre 2011, et combien a-t-il dépensé? Cela permet de vérifier la question d’Owen. Si les segments ont un sens, les meilleurs clients devraient revenir plus souvent et dépenser plus que les autres.
future_spend = future.groupby("customer")["amount"].sum()future_buyers = future.loc[~future["Invoice"].str.startswith("C"), "customer"].unique()rfm["came_back"] = rfm.index.isin(future_buyers)rfm["future_spend"] = future_spend.reindex(rfm.index).fillna(0)check = rfm.groupby("segment").agg(customers=("came_back", "size"), came_back=("came_back", "mean"), future_spend=("future_spend", "mean"), total=("future_spend", "sum"))check["share_of_future_spend"] = check["total"] / check["total"].sum()print(f"came back overall: {rfm['came_back'].mean():.1%}; " f"new customers in the six months, not segmented: {len(set(future_buyers) - set(rfm.index)):,}")check.drop(columns="total").sort_values("future_spend").round(3)Le résultat :
came back overall: 52.0%; new customers in the six months, not segmented: 919 customers came_back future_spend share_of_future_spendsegmentLapsed one-time buyers 1990 0.272 120.313 0.058Recent occasional buyers 718 0.666 486.751 0.084Quiet regulars 1424 0.577 536.349 0.184Best customers 761 0.926 3665.521 0.673Description textuelle du graphique
Un graphique à quatre colonnes. Acheteurs uniques inactifs : 27,2 % sont revenus. Acheteurs occasionnels récents : 66,6 %. Clients réguliers inactifs : 57,7 %. Meilleurs clients : 92,6 %.
Ce que cela nous dit. Les segments tiennent. Parmi les meilleurs clients, 92,6 % ont acheté de nouveau, contre 27,2 % des acheteurs inactifs. Les meilleurs clients ont dépensé 3 666 £ chacun, en moyenne. Ils représentent 67,3 % des dépenses des clients segmentés durant ces six mois. Avant la date de référence, ils en représentaient 65,8 %. Le service VIP vise donc les bonnes personnes.
Deux résultats demandent de la prudence. D’abord, 57,7 % des clients réguliers inactifs sont revenus sans aucune offre. Environ 820 des 1 424 auraient acheté de toute façon. Offrir un bon à tous réduit donc la marge pour la plupart. Ensuite, 919 nouveaux clients ont acheté durant les six mois. Aucune segmentation fondée sur le passé ne peut les inclure; ils ont besoin de leur propre plan d’accueil.
La vérification sur la période suivante est une seule vérification, faite une seule fois. Elle est honnête, car les segments n’ont utilisé aucune ligne de données futures. Si vous changez maintenant k et regardez de nouveau, la période future devient des données de validation. Il vous faut alors une nouvelle période, plus tardive, pour le test.
Les groupes comparés au score simple
Section intitulée « Les groupes comparés au score simple »Le regroupement (clustering) vaut-il sa complexité? Examinez le score RFM simple sur la même période future :
rfm.groupby("rfm_score").agg(customers=("came_back", "size"), came_back=("came_back", "mean"), future_spend=("future_spend", "mean")).round(3)Le résultat :
customers came_back future_spendrfm_score0 376 0.120 40.3171 528 0.244 104.0642 572 0.304 134.2403 537 0.363 211.3304 514 0.490 307.5695 513 0.507 317.5186 433 0.628 513.2537 418 0.734 863.7098 432 0.850 1097.7549 570 0.956 4391.723Description textuelle du graphique
Une courbe qui monte presque régulièrement de 12,0 % pour un score de 0 à 95,6 % pour un score de 9. Les valeurs sont 12,0, 24,4, 30,4, 36,3, 49,0, 50,7, 62,8, 73,4, 85,0 et 95,6 pour cent.
Ce que cela nous dit. Le score classe clairement les clients : chaque point supplémentaire augmente le taux de retour. Les clients avec 9 points sont revenus à 95,6 % et ont dépensé 4 392 £ chacun. C’est plus que le groupe des meilleurs clients.
Comparez maintenant les deux méthodes sur les mêmes questions. D’abord, à quel point chacune classe-t-elle bien les clients selon leur retour? Utilisez l’aire sous la courbe ROC, présentée dans Le compromis entre précision et rappel. C’est la probabilité qu’un client revenu soit classé au-dessus d’un client qui n’est pas revenu. Les segments sont classés selon leurs dépenses passées, tirées du profil, et non selon les données futures. Ensuite, examinez la vraie décision de Hannah : un service VIP pour 761 clients. Comparez le groupe des meilleurs clients aux 761 clients ayant le score le plus élevé.
from sklearn.metrics import roc_auc_score
value_rank = rfm["segment"].map(profile["monetary"].rank()) # the segments in order of past spendrankings = {"4 segments": value_rank, "RFM score 0-9": rfm["rfm_score"], "recency alone": -rfm["recency"]}for name, ranking in rankings.items(): print(f"{name:14} ROC AUC for 'came back': {roc_auc_score(rfm['came_back'], ranking):.3f}")
best = rfm[rfm["segment"] == "Best customers"]top_by_score = rfm.sort_values(["rfm_score", "monetary"], ascending=False).head(len(best))for name, group in [("Best customers", best), ("top by RFM score", top_by_score)]: print(f"{name:17} {len(group)} customers, came back {group['came_back'].mean():.1%}, " f"mean future spend £{group['future_spend'].mean():,.0f}, " f"{group['future_spend'].sum() / rfm['future_spend'].sum():.1%} of the future spend")print("customers on both lists:", len(best.index.intersection(top_by_score.index)))Le résultat :
4 segments ROC AUC for 'came back': 0.738RFM score 0-9 ROC AUC for 'came back': 0.791recency alone ROC AUC for 'came back': 0.764Best customers 761 customers, came back 92.6%, mean future spend £3,666, 67.3% of the future spendtop by RFM score 761 customers, came back 93.3%, mean future spend £3,697, 67.9% of the future spendcustomers on both lists: 682Ce que cela nous dit. Le score simple est aussi bon que les groupes, ou meilleur :
- Pour le classement, le score (0,791) dépasse les quatre segments (0,738). La récence seule (0,764) les dépasse aussi. La comparaison n’est pas tout à fait équitable : quatre groupes donnent seulement quatre niveaux, alors que le score en donne dix.
- Pour la décision VIP, les deux listes sont presque identiques : 682 des 761 clients figurent dans les deux. Les clients choisis par le score sont revenus un peu plus souvent (93,3 % contre 92,6 %) et ont dépensé un peu plus.
Qu’a donc ajouté le regroupement? Il a trouvé les quatre groupes et leurs frontières dans les données. Il a donné à Hannah quatre types de clients à nommer. Le score ne peut pas nommer un « client régulier inactif ». Mais pour choisir qui reçoit le service VIP, le score fait le même travail. Owen peut aussi le vérifier dans un tableur. C’est la leçon de Quand un modèle simple est meilleur. Quand la méthode simple est aussi bonne, elle est le meilleur choix. Plus de gens peuvent la comprendre, la vérifier et la maintenir.
La recommandation utilise les deux. Les noms des segments définissent le plan; le score choisit la liste VIP et classe les clients dans chaque segment.
Erreurs et sous-groupes
Section intitulée « Erreurs et sous-groupes »Un segment décrit un groupe, pas une personne. Deux types de surprises le montrent, et une analyse par sous-groupes vérifie les marchés :
country = clean.groupby("customer")["Country"].agg(lambda c: c.mode()[0])rfm["market"] = np.where(rfm.index.map(country) == "United Kingdom", "UK", "other")slices = rfm.groupby(["segment", "market"]).agg(customers=("came_back", "size"), came_back=("came_back", "mean"))print(slices.unstack().round(3))best_gone = (rfm["segment"] == "Best customers") & ~rfm["came_back"]lapsed_back = (rfm["segment"] == "Lapsed one-time buyers") & rfm["came_back"]print(f"Best customers who did not come back: {best_gone.sum()}; lapsed buyers who came back: {lapsed_back.sum()}")Le résultat :
customers came_backmarket UK other UK othersegmentBest customers 699 62 0.924 0.952Lapsed one-time buyers 1851 139 0.271 0.295Quiet regulars 1288 136 0.577 0.574Recent occasional buyers 653 65 0.662 0.708Best customers who did not come back: 56; lapsed buyers who came back: 542Ce que cela nous dit.
- Les segments fonctionnent de la même façon dans les deux marchés. Les taux de retour au Royaume-Uni et ailleurs diffèrent d’au plus 4,6 points. Les sous-groupes « autres » sont petits (62 à 139 clients). Une différence de quelques points peut donc y être due au hasard. Aucun marché n’a besoin de ses propres segments.
- 56 meilleurs clients ne sont pas revenus. C’est l’erreur la plus coûteuse : le service VIP était prévu pour eux, mais ils sont partis quand même. L’équipe de Hannah devrait les appeler; la raison pourrait être quelque chose qu’aucune colonne ne montre.
- 542 acheteurs inactifs sont revenus. Cela représente 27,2 % du groupe : trop pour les ignorer, mais trop peu pour justifier une offre coûteuse aux 1 990 clients. Un courriel peu coûteux représente un effort adapté.
Cas d’échec : le regroupement sans logarithme
Section intitulée « Cas d’échec : le regroupement sans logarithme »Erreur courante
Des groupes qui trouvent seulement les géants
Symptôme : Owen essaie le pipeline sans l’étape du logarithme. k-means s’exécute sans avertissement, et le profil ressemble à ceci :
no_log = make_pipeline(StandardScaler(), KMeans(n_clusters=4, n_init=10, random_state=0))wrong = rfm[RFM].assign(cluster=no_log.fit_predict(rfm[RFM]))wrong.groupby("cluster").agg(customers=("recency", "size"), recency=("recency", "median"), frequency=("frequency", "median"), monetary=("monetary", "median")) customers recency frequency monetarycluster0 2543 47.0 5.0 1411.071 2315 246.0 2.0 400.952 32 7.0 70.0 42475.773 3 14.0 100.0 354579.84Deux groupes comptent 32 et 3 clients. Les 4 858 autres clients sont seulement divisés entre « récents » et « non récents ».
Cause : la mise à l’échelle n’élimine pas une longue traîne. Après StandardScaler, les dépenses les plus élevées sont encore à plus de 30 écarts-types de la moyenne. k-means crée un groupe autour des quelques géants. Un seul géant éloigné ajoute plus à l’inertie que de nombreux clients ordinaires.
Correction : prenez le logarithme avant la mise à l’échelle, dans le même pipeline : make_pipeline(FunctionTransformer(np.log1p), StandardScaler(), KMeans(...)). Vérifiez ensuite la taille des groupes. Un groupe avec quelques membres, alors que vous attendiez des centaines, est un signe d’avertissement.
Conséquence dans le travail réel : une équipe présente « quatre types de clients ». Deux de ces types regroupent 35 clients que l’équipe des ventes connaît déjà par leur nom. La grande masse de clients au milieu reçoit un seul plan pour tout le monde.
Le journal des expériences
Section intitulée « Le journal des expériences »Gardez un journal de chaque essai, comme dans L’apprentissage automatique est itératif. Pour le regroupement, certaines colonnes changent. Il n’y a pas de score de validation croisée. Vous notez donc la silhouette, la stabilité et un bref résultat, comme la taille des groupes ou la vérification sur la période suivante.
from datetime import date
log = pd.DataFrame([ ("E01", "Baseline: quartile RFM score 0-9", None, None, "ROC AUC 0.791; top 761: 93.3% came back", "baseline", "Simple, and a person can compute it."), ("E02", "k-means k=4, scaled, no log", None, None, "two clusters of 32 and 3 customers", "reject", "A few very large customers decide the clusters."), ("E03", "k-means, log + scale, k=2 to 8", "k=2 0.456, k=4 0.378", "k=4 0.964", None, "keep", "k=4 is stable and has four actions."), ("E04", "k-means k=5, log + scale", 0.327, 0.866, None, "reject", "A middle group from Best and Quiet; no new action."), ("E05", "Final: k=4, checked on Jun-Nov 2011", 0.378, 0.964, "Best 92.6% came back, Lapsed 27.2%", "keep", "The segments hold in the next six months."),], columns=["id", "change", "silhouette", "stability", "result", "decision", "reason"])log.insert(1, "date", date.today().isoformat())log.to_csv("experiment_log.csv", index=False)log[["id", "change", "decision"]]Le résultat :
id change decision0 E01 Baseline: quartile RFM score 0-9 baseline1 E02 k-means k=4, scaled, no log reject2 E03 k-means, log + scale, k=2 to 8 keep3 E04 k-means k=5, log + scale reject4 E05 Final: k=4, checked on Jun-Nov 2011 keepCe que cela nous dit. Le journal contient toute l’histoire en cinq lignes, y compris les essais qui ont échoué. E04 est la variante que vous essayez dans le carnet (notebook) Jupyter. Avec k = 5, un groupe intermédiaire de bons clients réguliers se sépare des meilleurs clients et des clients réguliers inactifs. Aucune nouvelle action n’en découle.
Recommandations
Section intitulée « Recommandations »Vous rédigez la recommandation pour Hannah et Owen dans leurs mots. Les chiffres viennent de l’historique au 1 juin 2011 et de la vérification de juin à novembre 2011.
- Offrez un service VIP aux 761 meilleurs clients. Ils représentent 15,6 % des clients et 65,8 % des dépenses. Parmi eux, 92,6 % sont revenus. Ils ont généré 67,3 % des dépenses des six mois suivants. Pour choisir précisément les clients, utilisez le score RFM. Il donne la même liste pour 682 d’entre eux et se vérifie facilement.
- Appelez les meilleurs clients qui cessent de commander. Parmi eux, 56 ne sont pas revenus. Un meilleur client sans commande depuis 60 jours devrait recevoir un appel. N’attendez pas la prochaine segmentation.
- Testez l’offre de reconquête avant de l’envoyer à tous les clients réguliers peu actifs. Sans offre, 57,7 % sont revenus. Envoyez le bon de réduction à une moitié choisie au hasard parmi les 1 424 clients. Comparez les deux moitiés après trois mois. Seule cette comparaison montre si le bon change le comportement des clients.
- Entretenez la relation avec les 718 acheteurs occasionnels récents. Deux sur trois sont revenus. Un courriel mensuel présentant les nouveaux produits coûte peu. Certains deviendront de meilleurs clients.
- Envoyez seulement un rappel peu coûteux aux 1 990 acheteurs uniques inactifs. Parmi eux, 27,2 % sont revenus. Ils ont dépensé en moyenne 120 £ chacun.
- Préparez un plan d’accueil pour les nouveaux clients. Pendant ces six mois, 919 clients ont acheté pour la première fois. Ils n’ont aucun segment tant qu’ils n’ont pas d’historique.
À quoi les segments ne doivent pas servir. Ne les utilisez pas pour juger un client individuel. Parmi les meilleurs clients, 56 sont partis, et 542 clients inactifs sont revenus. Ne les utilisez pas pour fixer des limites de crédit ou des prix. Ils ont été créés uniquement pour orienter les efforts de marketing. Ne réutilisez pas ces segments sur des données ultérieures sans les reconstruire. Leurs frontières dépendent de la date de l’instantané.
Le carnet (notebook) Jupyter écrit la liste pour l’outil de courriel de Hannah :
deliverable = rfm[RFM + ["rfm_score", "segment"]].reset_index()deliverable.to_csv("segments.csv", index=False)print(deliverable["segment"].value_counts().to_string())deliverable.head(3)La sortie :
segmentLapsed one-time buyers 1990Quiet regulars 1424Best customers 761Recent occasional buyers 718 customer recency frequency monetary rfm_score segment0 12346 133 3 169.36 3 Lapsed one-time buyers1 12347 54 4 2434.96 7 Quiet regulars2 12348 56 4 1388.40 6 Quiet regularsCe que cela nous apprend. segments.csv contient une ligne par client, avec les trois nombres, le score et le segment. Regardez le client 12346 : une fréquence de 3 et un segment appelé « one-time » (acheteur unique). Une des trois commandes était la commande erronée de 74 215 bocaux. Elle compte quand même comme une visite. Les limites ci-dessous reviennent sur ce point.
Limites et prochaines étapes
Section intitulée « Limites et prochaines étapes »Les limites, dans l’ordre où Hannah devrait les connaître :
- Clients inscrits seulement. Parmi les lignes, 22,8 % n’ont pas d’identifiant client. S’il s’agit d’acheteurs invités, ils représentent une grande partie de la clientèle que les segments ne voient pas.
- Les segments décrivent; ils ne montrent pas les causes. La vérification sur la période suivante montre qui est revenu, pas ce qu’une offre changerait. Seul un test avec un groupe témoin peut montrer l’effet d’une offre (recommandation 3).
- Un instantané, une saison. L’historique se termine le 1 juin. La vérification couvre les mois avant Noël, la saison la plus occupée de la boutique. Un instantané au 1 décembre donnerait d’autres frontières. Reconstruisez les segments chaque trimestre et vérifiez chacun sur le trimestre suivant.
- Les choix de nettoyage changent les chiffres. Supprimer tous les doublons exacts peut supprimer certaines véritables commandes en double. Une commande annulée compte quand même comme une commande. Le client 12346 a une fréquence de 3, mais une de ces commandes était une erreur.
- Les groupes se chevauchent. Des valeurs de silhouette inférieures à 0,5 indiquent que les clients près d’une frontière pourraient appartenir à l’un ou l’autre groupe. La vérification de stabilité montre que la plupart des clients restent dans le même segment.
Prochaines étapes : ajoutez la répartition des produits, car un client qui achète seulement des articles de Noël se comporte différemment. Ajoutez le temps entre les commandes. Répétez la vérification sur la période suivante à partir d’une deuxième date d’instantané.
Faites-en un projet pour votre portfolio
Section intitulée « Faites-en un projet pour votre portfolio »Cette étude de cas peut devenir un projet public. Elle montre à un employeur tout le processus : données désordonnées, décisions, modèle, vérification honnête et recommandation d’affaires.
Quoi mettre dans le dépôt :
README.md, avec cinq courtes parties :- Problème : « Segmenter les clients d’une boutique en ligne pour le service VIP, les offres de reconquête et le suivi relationnel. »
- Données et licence : Online Retail II, UCI, CC BY 4.0, avec la référence et un lien vers la fiche du jeu de données.
- Approche : le tableau de nettoyage, RFM à une date d’instantané, logarithme + mise à l’échelle + k-moyennes. Le choix de k repose sur l’inertie, la silhouette et la stabilité.
- Résultats avec la référence de base : le tableau de vérification sur la période suivante et la comparaison avec le score RFM. Aire sous la courbe ROC de 0,791 contre 0,738; même liste VIP pour 682 des 761 clients.
- Limites : la liste ci-dessus, dans vos mots.
- Le carnet, exécuté depuis le début, avec ses sorties.
experiment_log.csvetsegments.csv. Le carnet écrit les deux dans le dossiersegment-online-shop-customers.- Une courte fiche de modèle pour la segmentation, comme dans Documenter les limites d’utilisation. Précisez l’utilisation prévue (efforts de marketing) et les utilisations exclues (crédit, prix, jugement d’un client individuel). Ajoutez les données, la vérification, les limites et le moment où il faudra reconstruire les segments.
Ne mettez pas le fichier de données dans votre dépôt. Ajoutez des liens vers la page UCI et la copie du cours, et conservez l’attribution.
Comment le décrire en entrevue, en deux phrases : « J’ai segmenté 4 893 clients d’une vraie boutique en ligne selon la récence, la fréquence et les dépenses, après avoir nettoyé un million de lignes de facture. Dans les six mois suivants, 92,6 % des meilleurs clients sont revenus contre 27,2 % des clients inactifs, et un simple score par quartiles a choisi la même liste VIP, alors j’ai recommandé la méthode plus simple pour cette décision. »
Pour aller plus loin :
- Répétez toute l’analyse à une deuxième date d’instantané (par exemple le 1 décembre 2010) et comparez les segments.
- Ajoutez un quatrième nombre, comme le nombre moyen de jours entre les commandes, et voyez si les segments changent.
- Essayez un modèle de mélange gaussien plutôt que les k-moyennes et comparez la vérification sur la période suivante.
- Prédisez les dépenses de chaque client dans les six mois suivants avec un modèle de régression. Comparez le classement avec les segments et le score.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Pourquoi faut-il déduire les retours des dépenses dans ces données? Qu'arrive-t-il au client 12346 si vous supprimez plutôt les lignes de retour?
Afficher la réponse
Le client 12346 a commandé 74 215 bocaux de rangement pour 77 184 £ et annulé la commande 16 minutes plus tard. Avec les dépenses nettes, la commande et son retour s’annulent, et les dépenses du client sont de 169,36 £. Si vous supprimez les lignes de retour et gardez la commande, ce client semble être un des plus gros acheteurs. Ce client pourrait alors recevoir un service VIP. Les dépenses nettes, soit les achats moins les retours, mesurent ce que le client a réellement acheté.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
La silhouette est maximale à k = 2 (0,456). Pourquoi avez-vous choisi k = 4? Quelles preuves avez-vous utilisées en plus de la silhouette?
Afficher la réponse
Deux groupes diraient seulement « actifs » et « inactifs ». Hannah a trois outils, plus un groupe qui ne reçoit presque rien. À k = 4, la silhouette présente un maximum local (0,378), et l’inertie forme un coude. Les segments sont aussi stables : les mêmes clients se retrouvent ensemble dans différents échantillons de 80 %. L’ARI est de 0,964, contre 0,866 à k = 5. Les données suggèrent de bonnes séparations; l’entreprise décide laquelle est utile. Toutes les valeurs de silhouette sont inférieures à 0,5, donc les groupes se touchent.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Owen dit : le score RFM par quartiles fait aussi bien que les groupes, donc le regroupement était inutile. Que répondez-vous?
Afficher la réponse
Pour la liste VIP, Owen a raison. Le score classe mieux les clients selon leur retour (aire sous la courbe ROC de 0,791 contre 0,738). Il donne la même liste pour 682 des 761 clients. L’équipe devrait donc utiliser le score pour cette décision. Mais le regroupement (clustering) a trouvé les quatre types de clients et leurs frontières dans les données. Il a donné à Hannah des groupes à nommer et pour lesquels planifier, comme les clients réguliers peu actifs. Le score seul ne dit pas quels clients ont besoin de quel traitement. Utilisez les segments pour le plan et le score pour classer les clients à l’intérieur de ce plan. Cette comparaison était possible seulement parce que vous aviez d’abord construit la référence de base.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
Dans cette étude de cas, vous avez transformé 1 067 371 lignes de facture en segments utilisables par une équipe CRM :
- Le nettoyage représentait la majeure partie du travail. Vous avez supprimé 34 335 doublons exacts, surtout issus de deux feuilles qui se chevauchaient. Vous avez aussi supprimé 235 151 lignes sans client, 3 641 lignes sans produit et 61 lignes au prix nul ou négatif. Chaque suppression avait sa raison. Vous avez gardé les retours pour obtenir des dépenses nettes.
- RFM à une date d’instantané : 4 893 clients, avec seulement l’historique avant le 1 juin 2011. Les dépenses sont très asymétriques : les 10 % de clients en tête en ont généré 61,4 %. Vous avez donc appliqué le logarithme avant la mise à l’échelle.
- Choisir k avec trois types de preuves : le coude d’inertie, un maximum local de silhouette (0,378) et la stabilité entre échantillons (0,964) indiquaient tous k = 4. L’entreprise était d’accord.
- La vérification sur la période suivante : 92,6 % des meilleurs clients ont acheté de nouveau dans les six mois suivants. C’était le cas de 27,2 % des acheteurs inactifs.
- Le score simple faisait aussi bien : le score RFM par quartiles classait mieux les clients selon leur retour (0,791 contre 0,738). Il a choisi la même liste VIP pour 682 des 761 clients.
Ensuite, un jeu de données plus petit avec un autre piège. Les dépenses des clients grossistes sont si asymétriques que la meilleure silhouette mène à une réponse inutile.
Leçon suivante : Trouver les types d'acheteurs en gros