0% ont trouvé ce document utile (0 vote)
1 vues17 pages

Data Exploration

Le document présente une analyse exploratoire des données pour un challenge de segmentation des clients sur Kaggle, visant à classifier les clients en quatre segments. Il met en évidence un problème de fuite de données, avec 89% des IDs du jeu de test présents dans le jeu d'entraînement, et analyse les différences entre les caractéristiques des clients dans les deux ensembles de données. Des stratégies de modélisation sont proposées pour traiter les données manquantes et les évolutions temporelles des caractéristiques des clients.

Transféré par

Ammar
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
1 vues17 pages

Data Exploration

Le document présente une analyse exploratoire des données pour un challenge de segmentation des clients sur Kaggle, visant à classifier les clients en quatre segments. Il met en évidence un problème de fuite de données, avec 89% des IDs du jeu de test présents dans le jeu d'entraînement, et analyse les différences entre les caractéristiques des clients dans les deux ensembles de données. Des stratégies de modélisation sont proposées pour traiter les données manquantes et les évolutions temporelles des caractéristiques des clients.

Transféré par

Ammar
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

data-exploration

November 3, 2025

1 Analyse exploratoire
1.1 Challenge Kaggle: Segmentation Clients
Objectif: Comprendre les données et identifier les patterns pour classifier les clients en 4 segments
(A, B, C, D) sur le fichier de test.L’objectif est de maximiser l’accuracy du test

1.2 [Link] et setups


[ ]: import pandas as pd
import numpy as np
import [Link] as px
import plotly.graph_objects as go
import warnings
[Link]('ignore')

1.3 2. Data loading


[ ]: # Train
train_df = pd.read_csv('../Data/[Link]')
print(f'Train: {train_df.shape}')
# Test
test_df = pd.read_csv('../Data/[Link]')
print(f'Test: {test_df.shape}')

Train: (8068, 11)


Test: (2627, 10)
Le fichier de test ne contient pas la target qui est la segmentation A,B,C et D
Nous nous concentrons pour le moment sur les données d’entraînement, puis nous comparerons
ensuite la distribution entre l’entraînement et le test.

1.4 3. Exploration du fichier d’entrainement


[ ]: train_df.head()

[ ]: ID Gender Ever_Married Age Graduated Profession Work_Experience \


0 462809 Male No 22 No Healthcare 1.0

1
1 462643 Female Yes 38 Yes Engineer NaN
2 466315 Female Yes 67 Yes Engineer 1.0
3 461735 Male Yes 67 Yes Lawyer 0.0
4 462669 Female Yes 40 Yes Entertainment NaN

Spending_Score Family_Size Var_1 Segmentation


0 Low 4.0 Cat_4 D
1 Average 3.0 Cat_4 A
2 Low 1.0 Cat_6 B
3 High 2.0 Cat_6 B
4 High 6.0 Cat_6 A

Il y a 10 variables explicatives (features) et 1 variable cible (target). - L’ID est une clé unique pour
chaque individu, il ne sera donc pas utilisé comme feature par la suite. On vérifie ci-après qu’il y a
bien autant de valeurs uniques que de lignes, ce qui confirme qu’il s’agit d’une clé primaire. - Toutes
les variables sont facilement interprétables, à l’exception de la variable Var_1 dont la signification
n’est pas précisée.
[ ]: print(train_df['ID'].nunique())
print(test_df['ID'].nunique())

Il y a 2332 IDs en commun entre le fichier d'entraînement et le fichier test.

[ ]: # Vérifier s'il y a une intersection entre les IDs les train et le test
intersection = set(train_df['ID']).intersection(set(test_df['ID']))
print(len(intersection))

2332

1.4.1 3.1 ANALYSE DES IDs COMMUNS - Data Leakage potentiel


Problème découvert : 2332 IDs sont présents à la fois dans le train et le test set, soit 89% du
test !
[ ]: # Statistiques sur les IDs communs
print(f"\nIDs communs: {len(intersection)}")
print(f"% du test set: {len(intersection)/len(test_df)*100:.1f}%")
print(f"% du train set: {len(intersection)/len(train_df)*100:.1f}%")
print("="*80)

IDs communs: 2332


% du test set: 88.8%
% du train set: 28.9%
================================================================================

[ ]: # Liste des features à comparer sur les IDs en commun


features = ['Gender', 'Ever_Married', 'Age', 'Graduated', 'Profession',
'Work_Experience', 'Spending_Score', 'Family_Size', 'Var_1']

2
# Dictionnaire pour stocker les statistiques
feature_stats = {}

for feat in features:


diff_count = 0

for common_id in intersection:


train_row = train_df[train_df['ID'] == common_id].iloc[0]
test_row = test_df[test_df['ID'] == common_id].iloc[0]

train_val = train_row[feat]
test_val = test_row[feat]

# Comparer en gérant les NaN


both_nan = [Link](train_val) and [Link](test_val)
both_equal = (train_val == test_val if not [Link](train_val) and not␣
↪[Link](test_val) else False)

if not (both_nan or both_equal):


diff_count += 1

# Calculer le pourcentage
pct_diff = (diff_count / len(intersection)) * 100
feature_stats[feat] = {
'Nb_Differences': diff_count,
'Pourcentage': round(pct_diff, 2)
}

stats_df = [Link](feature_stats).T
stats_df = stats_df.sort_values('Pourcentage', ascending=False)

print(stats_df)

Nb_Differences Pourcentage
Age 2059.0 88.29
Work_Experience 1142.0 48.97
Gender 0.0 0.00
Ever_Married 0.0 0.00
Graduated 0.0 0.00
Profession 0.0 0.00
Spending_Score 0.0 0.00
Family_Size 0.0 0.00
Var_1 0.0 0.00
Synthèse : Analyse des différences par feature pour les IDs communs
Features stables (0% de différence) : - Gender, Ever_Married, Graduated, Profession, Spend-

3
ing_Score, Family_Size, Var_1 - Ces caractéristiques restent identiques pour un même individu
entre Train et Test
Features variables : - Age : 88.29% de différences - Work_Experience : 48.97% de différences
Interprétation : Les données représentent une évolution temporelle des mêmes clients. Seuls l’âge
et l’expérience professionnelle changent, ce qui suggère que les données Test ont été collectées après
les données Train.
Implications pour la modélisation plus tard : - 136 lignes (5.8% du test) : features iden-
tiques, segmentation probablement inchangée - 2196 lignes (83.6% du test) : évolution temporelle
(âge/expérience), segmentation à prédire en tenant compte de cette évolution - 295 lignes (11.2%
du test) : IDs non présents dans Train, nécessitent un modèle de ML classique
Une stratégie possible (stratégie n°1) consiste à adopter une approche hybride : pour les IDs du
test set présents aussi dans le train, on réalise un “lookup” direct pour retrouver leur segmentation
à partir du train (les features étant identiques ou quasiment identiques). Pour les IDs restants
(absents du train), la segmentation devra être prédite à l’aide d’un modèle de machine learning
classique. Cette première approche sera comparée à une approche n°2 où l’ensemble des prédictions
du test est réalisé uniquement par un modèle de ML, sans lookup. Le choix final du modèle sera
discuté plus tard.

1.4.2 3.2 Analyse du fichier de train

[ ]: train_df.describe()

[ ]: ID Age Work_Experience Family_Size


count 8068.000000 8068.000000 7239.000000 7733.000000
mean 463479.214551 43.466906 2.641663 2.850123
std 2595.381232 16.711696 3.406763 1.531413
min 458982.000000 18.000000 0.000000 1.000000
25% 461240.750000 30.000000 0.000000 2.000000
50% 463472.500000 40.000000 1.000000 3.000000
75% 465744.250000 53.000000 4.000000 4.000000
max 467974.000000 89.000000 14.000000 9.000000

[ ]: train_df.describe(include='all')

[ ]: ID Gender Ever_Married Age Graduated Profession \


count 8068.000000 8068 7928 8068.000000 7990 7944
unique NaN 2 2 NaN 2 9
top NaN Male Yes NaN Yes Artist
freq NaN 4417 4643 NaN 4968 2516
mean 463479.214551 NaN NaN 43.466906 NaN NaN
std 2595.381232 NaN NaN 16.711696 NaN NaN
min 458982.000000 NaN NaN 18.000000 NaN NaN
25% 461240.750000 NaN NaN 30.000000 NaN NaN
50% 463472.500000 NaN NaN 40.000000 NaN NaN
75% 465744.250000 NaN NaN 53.000000 NaN NaN

4
max 467974.000000 NaN NaN 89.000000 NaN NaN

Work_Experience Spending_Score Family_Size Var_1 Segmentation


count 7239.000000 8068 7733.000000 7992 8068
unique NaN 3 NaN 7 4
top NaN Low NaN Cat_6 D
freq NaN 4878 NaN 5238 2268
mean 2.641663 NaN 2.850123 NaN NaN
std 3.406763 NaN 1.531413 NaN NaN
min 0.000000 NaN 1.000000 NaN NaN
25% 0.000000 NaN 2.000000 NaN NaN
50% 1.000000 NaN 3.000000 NaN NaN
75% 4.000000 NaN 4.000000 NaN NaN
max 14.000000 NaN 9.000000 NaN NaN

Les profils sont majoritairement masculins (55%), mariés (59%) et diplômés (62%). La profession
la plus fréquente est Artist (32%). On observe un déséquilibre notable dans certaines variables :
60% ont un score de dépense Low et 65% appartiennent à la catégorie Cat_6 pour Var_1.
Les valeurs manquantes sont présentes principalement dans Work_Experience (10.3%) et Fam-
ily_Size (4.2%).

1.4.3 3.3 Analyse des valeurs manquantes

[ ]: # Calcul des valeurs manquantes


missing = train_df.isnull().sum()
missing_pct = (missing / len(train_df)) * 100

missing_df = [Link]({
'Nombre': missing,
'Pourcentage': missing_pct.round(2)
})

# Filtrer uniquement les colonnes avec des valeurs manquantes


missing_df = missing_df[missing_df['Nombre'] > 0].sort_values('Nombre',␣
↪ascending=False)

print("Valeurs manquantes dans le fichier Train :\n")


print(missing_df)

Valeurs manquantes dans le fichier Train :

Nombre Pourcentage
Work_Experience 829 10.28
Family_Size 335 4.15
Ever_Married 140 1.74
Profession 124 1.54
Graduated 78 0.97

5
Var_1 76 0.94

[ ]: # Exemples de lignes avec Work_Experience manquante


display(train_df[train_df['Work_Experience'].isnull()].head(5))

ID Gender Ever_Married Age Graduated Profession \


1 462643 Female Yes 38 Yes Engineer
4 462669 Female Yes 40 Yes Entertainment
13 459573 Male Yes 70 No Lawyer
39 467442 Male Yes 56 Yes Artist
45 463156 Female Yes 79 No Lawyer

Work_Experience Spending_Score Family_Size Var_1 Segmentation


1 NaN Average 3.0 Cat_4 A
4 NaN High 6.0 Cat_6 A
13 NaN Low 1.0 Cat_6 A
39 NaN Average 2.0 Cat_6 C
45 NaN High 2.0 Cat_6 A
Il est surprenant de voir, par exemple, des avocats âgés de 79 ans ou des personnes de plus de 30
ans sans aucune information sur leur expérience professionnelle. Comme montré précédemment, la
présence de NaN dans “Work_Experience” ne correspond pas à une absence d’expérience (valeur
0) mais plutôt à un champ non renseigné, car la valeur 0 est bien utilisée ailleurs dans le jeu de
données.
Conséquence : Il sera nécessaire de déterminer une approche adaptée, par exemple en supprimant
les lignes concernées avec les données manquates ou en mettant en place une méthode d’imputation,
afin d’assurer au modèle des données propres pour l’apprentissage.
[ ]: # On veut reagrder s'il existe des patterns parmi les individus qui ont leur␣
↪Work_Experience manquante

display(train_df[train_df['Work_Experience'].isnull()].describe(include='all'))

ID Gender Ever_Married Age Graduated Profession \


count 829.000000 829 806 829.000000 817 803
unique NaN 2 2 NaN 2 9
top NaN Male Yes NaN Yes Artist
freq NaN 463 481 NaN 448 211
mean 462981.407720 NaN NaN 43.494572 NaN NaN
std 2691.892632 NaN NaN 17.782069 NaN NaN
min 459001.000000 NaN NaN 18.000000 NaN NaN
25% 460630.000000 NaN NaN 30.000000 NaN NaN
50% 462779.000000 NaN NaN 39.000000 NaN NaN
75% 464964.000000 NaN NaN 53.000000 NaN NaN
max 467902.000000 NaN NaN 89.000000 NaN NaN

Work_Experience Spending_Score Family_Size Var_1 Segmentation


count 0.0 829 764.000000 820 829
unique NaN 3 NaN 7 4

6
top NaN Low NaN Cat_6 D
freq NaN 473 NaN 499 288
mean NaN NaN 2.904450 NaN NaN
std NaN NaN 1.557977 NaN NaN
min NaN NaN 1.000000 NaN NaN
25% NaN NaN 2.000000 NaN NaN
50% NaN NaN 3.000000 NaN NaN
75% NaN NaN 4.000000 NaN NaN
max NaN NaN 9.000000 NaN NaN

[ ]: # Exemples de lignes avec Family_Size manquante


display(train_df[train_df['Family_Size'].isnull()].head(5))

ID Gender Ever_Married Age Graduated Profession Work_Experience \


12 461230 Female No 19 No Executive 0.0
33 467010 Male No 26 No Homemaker 9.0
59 460881 Male Yes 72 Yes Lawyer 1.0
112 467758 Female Yes 50 Yes Doctor 1.0
126 466295 Female Yes 42 No Engineer 0.0

Spending_Score Family_Size Var_1 Segmentation


12 Low NaN Cat_3 D
33 Low NaN Cat_6 D
59 Low NaN Cat_4 D
112 Low NaN Cat_6 B
126 Low NaN Cat_6 A
Observations importantes :
Les lignes avec Work_Experience manquante présentent des différences notables : - Moins de
diplômés (55% vs 62% dans le dataset global) - Surreprésentation du segment D (35% vs 28% dans
le dataset global)
Ces valeurs manquantes ne semblent pas aléatoires mais liées au profil des individus (moins éduqués,
segment D plus fréquent).
Décision : Ne pas supprimer les valeurs manquantes
La suppression de toutes les lignes avec valeurs manquantes entraînerait une perte significative de
données (environ 17 % du dataset). De plus, nous avons observé que les valeurs manquantes de
Work_Experience ne sont pas aléatoires mais corrélées avec le profil des individus (moins diplômés,
segment D surreprésenté).
Stratégie retenue : - Conserver toutes les lignes - Utiliser LightGBM qui gère nativement les valeurs
manquantes - Les valeurs manquantes contiennent de l’information que le modèle peut exploiter
[ ]: # Impact de la suppression des lignes avec valeurs manquantes
print("Dataset initial :", train_df.shape)

# Supprimer toutes les lignes avec au moins une valeur manquante


train_clean = train_df.dropna()

7
print(f"Dataset sans valeurs manquantes : {train_clean.shape}")
print(f"Lignes supprimées : {len(train_df) - len(train_clean)} ({(len(train_df)␣
↪- len(train_clean))/len(train_df)*100:.1f}%)")

# Distribution de la target avant/après


print("\nDistribution de Segmentation :")
print("\nAvant suppression :")
print(train_df['Segmentation'].value_counts(normalize=True).sort_index().
↪round(3))

print("\nAprès suppression :")


print(train_clean['Segmentation'].value_counts(normalize=True).sort_index().
↪round(3))

Dataset initial : (8068, 11)


Dataset sans valeurs manquantes : (6665, 11)
Lignes supprimées : 1403 (17.4%)

Distribution de Segmentation :

Avant suppression :
Segmentation
A 0.244
B 0.230
C 0.244
D 0.281
Name: proportion, dtype: float64

Après suppression :
Segmentation
A 0.242
B 0.236
C 0.258
D 0.264
Name: proportion, dtype: float64
On perd 17% des données mais on gagne en cohérence des données et l’impact sur la distribution
de la target est faible

1.4.4 3.4 Analyse univariée dans le train set

[ ]: target_df = train_df['Segmentation'].value_counts().reset_index()
target_df.columns = ['Segment', 'Count']
print(target_df)

Segment Count
0 D 2268
1 A 1972
2 C 1970

8
3 B 1858

[ ]: fig = [Link](target_df, values='Count', names='Segment',


title='Proportion des Segments')
[Link]()

Répartion bien balancée de la target


[ ]: df_gender = train_df['Gender'].value_counts(normalize=True).reset_index()
df_gender.columns = ['Gender', 'Percentage']
df_gender['Percentage'] = (df_gender['Percentage'] * 100).round(1) #␣
↪Pourcentage arrondi à 0.1%

fig = [Link](df_gender, values='Percentage', names='Gender', title='Répartition␣


↪(%) par Genre', hole=0.3)

[Link]()

Plus d’hommes que de femmes


[ ]: df_married = train_df['Ever_Married'].value_counts().reset_index()
df_married.columns = ['Ever_Married', 'Count']
df_married['Percentage'] = (df_married['Count'] / df_married['Count'].sum() *␣
↪100).round(1)

fig = [Link](df_married, values='Count', names='Ever_Married',␣


↪title='Répartition Statut Marital (%)', hole=0.3)

[Link]()

df_graduated = train_df['Graduated'].value_counts().reset_index()
df_graduated.columns = ['Graduated', 'Count']
df_graduated['Percentage'] = (df_graduated['Count'] / df_graduated['Count'].
↪sum() * 100).round(1)

fig = [Link](df_graduated, values='Count', names='Graduated',␣


↪title='Répartition Diplôme (%)', hole=0.3)

[Link]()

Population majoritairement mariée et diplômée


[ ]: df_prof = train_df['Profession'].value_counts().reset_index()
df_prof.columns = ['Profession', 'Count']
fig = [Link](df_prof, x='Profession', y='Count', title='Profession',␣
↪color='Profession', text='Count')

fig.update_layout(xaxis_tickangle=-45)
[Link]()

Artistes surreprésentés près de 25 %. Il ne s’agit donc pas d’un échantillon représentatid de la


population française mai sd’un epopulation de niche
[ ]: df_spend = train_df['Spending_Score'].value_counts().reset_index()
df_spend.columns = ['Spending_Score', 'Count']

9
fig = [Link](df_spend, x='Spending_Score', y='Count', title='Score de Dépense',
color='Spending_Score', text='Count',
category_orders={'Spending_Score': ['Low', 'Average', 'High']})
[Link]()

Population peu dépensière en majorité


[ ]: df_var1 = train_df['Var_1'].value_counts().reset_index()
df_var1.columns = ['Var_1', 'Count']
fig = [Link](df_var1, x='Var_1', y='Count', title='Var_1', color='Var_1',␣
↪text='Count')

[Link]()

Cat 6 surrepésentées et et les autres classes sont minoraitaires. En regardant plus en détail les
catégories je ne trouve pas de patterns particuliers
[ ]: # Analyse de Cat_5 (catégorie la plus rare)
cat5_df = train_df[train_df['Var_1'] == 'Cat_5']

print(f"Nombre d'observations Cat_5 : {len(cat5_df)} ({len(cat5_df)/


↪len(train_df)*100:.2f}% du dataset)\n")

# Statistiques descriptives
cat5_df.describe(include='all')

Nombre d'observations Cat_5 : 85 (1.05% du dataset)

[ ]: ID Gender Ever_Married Age Graduated Profession \


count 85.000000 85 83 85.000000 84 84
unique NaN 2 2 NaN 2 9
top NaN Female No NaN Yes Artist
freq NaN 52 44 NaN 43 23
mean 463358.247059 NaN NaN 37.164706 NaN NaN
std 2166.656009 NaN NaN 13.464987 NaN NaN
min 459167.000000 NaN NaN 18.000000 NaN NaN
25% 461701.000000 NaN NaN 26.000000 NaN NaN
50% 463217.000000 NaN NaN 33.000000 NaN NaN
75% 464493.000000 NaN NaN 43.000000 NaN NaN
max 467912.000000 NaN NaN 78.000000 NaN NaN

Work_Experience Spending_Score Family_Size Var_1 Segmentation


count 82.000000 85 79.000000 85 85
unique NaN 3 NaN 1 4
top NaN Low NaN Cat_5 D
freq NaN 58 NaN 85 28
mean 2.878049 NaN 3.594937 NaN NaN
std 3.563845 NaN 2.016083 NaN NaN

10
min 0.000000 NaN 1.000000 NaN NaN
25% 0.000000 NaN 2.000000 NaN NaN
50% 1.000000 NaN 3.000000 NaN NaN
75% 5.000000 NaN 5.000000 NaN NaN
max 14.000000 NaN 9.000000 NaN NaN

[ ]: # Analyse de Cat_6 (catégorie la plus représentées)


cat6_df = train_df[train_df['Var_1'] == 'Cat_6']

print(f"Nombre d'observations Cat_6 : {len(cat6_df)} ({len(cat6_df)/


↪len(train_df)*100:.2f}% du dataset)\n")

# Statistiques descriptives
cat6_df.describe(include='all')

Nombre d'observations Cat_6 : 5238 (64.92% du dataset)

[ ]: ID Gender Ever_Married Age Graduated Profession \


count 5238.000000 5238 5177 5238.000000 5204 5179
unique NaN 2 2 NaN 2 9
top NaN Male Yes NaN Yes Artist
freq NaN 2946 3190 NaN 3574 1844
mean 463444.409317 NaN NaN 45.892707 NaN NaN
std 2710.315664 NaN NaN 17.414960 NaN NaN
min 458982.000000 NaN NaN 18.000000 NaN NaN
25% 461235.250000 NaN NaN 32.000000 NaN NaN
50% 463411.500000 NaN NaN 42.000000 NaN NaN
75% 465904.750000 NaN NaN 57.000000 NaN NaN
max 467973.000000 NaN NaN 89.000000 NaN NaN

Work_Experience Spending_Score Family_Size Var_1 Segmentation


count 4739.000000 5238 5064.000000 5238 5238
unique NaN 3 NaN 1 4
top NaN Low NaN Cat_6 C
freq NaN 3050 NaN 5238 1496
mean 2.709010 NaN 2.635071 NaN NaN
std 3.490194 NaN 1.369630 NaN NaN
min 0.000000 NaN 1.000000 NaN NaN
25% 0.000000 NaN 2.000000 NaN NaN
50% 1.000000 NaN 2.000000 NaN NaN
75% 5.000000 NaN 4.000000 NaN NaN
max 14.000000 NaN 9.000000 NaN NaN

[ ]: fig = [Link](train_df, x='Age', title='Distribution Age', nbins=30)


[Link]()
fig = [Link](train_df, y='Age', title='Boxplot Age')

11
[Link]()

Pas de valeurs aberrantes


[ ]: fig = [Link](train_df, x='Work_Experience', title='Distribution␣
↪Expérience', nbins=20)

[Link]()

Il est étonnant que les années d’expériences soient concentrés autour de [0,2] sachant que l’age
médian est de 42 ans. Problèmes dans les données.
[ ]: fig = [Link](train_df, x='Family_Size', title='Distribution Taille␣
↪Famille', nbins=10)

[Link]()

Rien à redire
[ ]: fig = [Link](train_df, x='Gender', color='Segmentation',
title='Genre vs Segmentation', barmode='group')
[Link]()

Pas de différence de proportion de la targer selon les 2 genres


[ ]: fig = [Link](train_df, x='Ever_Married', color='Segmentation',
title='Statut Marital vs Segmentation', barmode='group')
[Link]()

1. Corrélation inverse marquée Non mariés → Segment D (comportement opposé à C) Mariés


→ Segment C (comportement opposé à D)
2. Pattern très clair Le statut marital semble être un fort prédicteur de la segmentation, avec
un pattern quasi-opposé entre mariés et non-mariés.
[ ]: fig = [Link](train_df, x='Profession', color='Segmentation',
title='Profession vs Segmentation', barmode='group')
fig.update_layout(xaxis_tickangle=-45)
[Link]()

Artist - LA profession dominante C’est de loin la profession la plus fréquente (~2500 total = 32%
du dataset) - Segment C dominant : ~1100 (le plus élevé) - Segment B : ~750 - Segment A : ~550
- Segment D très faible : ~120
Healthcare - Pattern opposé à Artist - 2ème profession la plus fréquente (~1200 total) - Segment
D écrasant : ~1000 - Segments A, B, C très minoritaires (~100 chacun)

[ ]: fig = [Link](train_df, x='Spending_Score', color='Segmentation',


title='Score Dépense vs Segmentation', barmode='group',
category_orders={'Spending_Score': ['Low', 'Average',␣
↪'High']})

[Link]()

12
1. Corrélation inverse claire Low Spending → Segment D (dépensiers faibles) Average Spending
→ Segment C (dépensiers modérés) High Spending → Distribution équilibrée A, B, C
2. Pattern très discriminant Le Segment D se distingue clairement : Dominé par Low spenders
(~1950) Quasi absent chez Average/High (~300 au total)

[ ]: fig = [Link](train_df, x='Segmentation', y='Age', color='Segmentation',


title='Age par Segment')
[Link]()

• Segment D - LES PLUS JEUNES Médiane : ~30 ans Q1-Q3 : 22-38 ans Population jeune,
début de carrière
• Segment A - ÂGE MOYEN Médiane : ~42 ans Q1-Q3 : 33-52 ans Transition vers la maturité
• Segments B & C - LES PLUS ÂGÉS Médiane : ~48-50 ans Q1-Q3 : 38-59 ans Population
mature, carrière établie
[ ]: fig = [Link](train_df, x='Segmentation', y='Work_Experience',␣
↪color='Segmentation',

title='Expérience par Segment')


[Link]()

Peu discriminant
Peu discriminant pour trouver la segmentation
[ ]: fig = [Link](train_df, x='Segmentation', y='Family_Size', color='Segmentation',
title='Taille Famille par Segment')
[Link]()

Peu discriminant
[ ]: numerical_cols = ['Age', 'Work_Experience', 'Family_Size']
corr = train_df[numerical_cols].corr()
print(corr)
fig = [Link](corr, text_auto='.2f', title='Matrice de Corrélation',
color_continuous_scale='RdBu_r')
[Link]()

Age Work_Experience Family_Size


Age 1.000000 -0.190789 -0.280517
Work_Experience -0.190789 1.000000 -0.063234
Family_Size -0.280517 -0.063234 1.000000

[ ]: # Ce qui est bizarre c'est que la taille de la famille est négativement␣


↪corrélée à l'âge

# On va regarder si c'est vraiment le cas


fig = [Link](train_df, x='Family_Size', y='Age', title='Taille Famille vs␣
↪Âge',

hover_data=['ID', 'Segmentation'])
[Link]()

13
# Ce qui est bizarre c'est que l'expérience est négativement corrélée à l'âge
# On va regarder si c'est vraiment le cas
fig = [Link](train_df, x='Work_Experience', y='Age', title='Expérience vs␣
↪Âge',

hover_data=['ID', 'Segmentation'])
[Link]()

On voit clairementque les données ne sont pas cohénrentes avec des personnages agées de 18 ans
avec 14 ans d’expérience On va regarder si on peut trouver des patterns parmi les individus qui ont
des valeurs aberrantes
[ ]: fig = [Link](train_df, x='Work_Experience', y='Age', title='Expérience vs␣
↪Âge',

hover_data=['ID', 'Segmentation'])
[Link]()

[ ]: train_df.query('Age <= 18 and Work_Experience >= 14')

[ ]: ID Gender Ever_Married Age Graduated Profession Work_Experience \


2722 467570 Male No 18 No Healthcare 14.0

Spending_Score Family_Size Var_1 Segmentation


2722 Low NaN Cat_6 D

[ ]: #Données aberrantes
train_df.query('Age <= 18 and Work_Experience >= 6').describe(include='all')

[ ]: ID Gender Ever_Married Age Graduated Profession \


count 24.000000 24 23 24.0 24 23
unique NaN 2 2 NaN 1 3
top NaN Male No NaN No Healthcare
freq NaN 14 22 NaN 24 21
mean 463884.916667 NaN NaN 18.0 NaN NaN
std 2892.195329 NaN NaN 0.0 NaN NaN
min 458986.000000 NaN NaN 18.0 NaN NaN
25% 461617.000000 NaN NaN 18.0 NaN NaN
50% 464842.500000 NaN NaN 18.0 NaN NaN
75% 466541.500000 NaN NaN 18.0 NaN NaN
max 467914.000000 NaN NaN 18.0 NaN NaN

Work_Experience Spending_Score Family_Size Var_1 Segmentation


count 24.000000 24 23.000000 24 24
unique NaN 2 NaN 5 2
top NaN Low NaN Cat_6 D
freq NaN 23 NaN 13 23
mean 8.250000 NaN 4.173913 NaN NaN
std 2.288915 NaN 1.696311 NaN NaN

14
min 6.000000 NaN 1.000000 NaN NaN
25% 6.750000 NaN 3.000000 NaN NaN
50% 8.000000 NaN 4.000000 NaN NaN
75% 9.000000 NaN 5.000000 NaN NaN
max 14.000000 NaN 9.000000 NaN NaN

On remarque ici par exemple parmi les données aberrantes que la ceux ayant de 18 ans avec plus
de 6 ans d’expérience travaillent dans le domaine médical, ne sont pas diplômés et sont dans le
segment D.

1.5 4. Stratégies de modélisation


Avant de proposer des stratgéies pour fournir les prédictions de segmentation du dataframe de test.
Il faut vérifier si les distributions des features entre le train et le test sont les mêmes
Test statistiques pour vérifier si les distributions des features entre le train et le test sont les mêmes
[ ]: from [Link] import ks_2samp

print('� Test de Kolmogorov-Smirnov (Variables Numériques)')


print('Hypothèse: Les distributions Train et Test sont identiques')

results = []
for col in numerical_cols:
train_data = train_df[col].dropna()
test_data = test_df[col].dropna()
statistic, p_value = ks_2samp(train_data, test_data)

status = 'OK' if p_value > 0.05 else '� ATTENTION'


[Link]({
'Variable': col,
'KS_Statistic': f'{statistic:.4f}',
'p_value': f'{p_value:.4f}',
'Status': status
})
print(f'{col:20} | KS={statistic:.4f} | p-value={p_value:.4f} | {status}')

print('\n' + '='*80)
print('Interprétation:')
print('- p-value élevée (>0.05): Les distributions sont statistiquement␣
↪similaires')

print('- p-value faible (<0.05): Les distributions diffèrent significativement')

[ ]: # Liste des features catégorielles à analyser


categorical_features = ['Gender', 'Ever_Married', 'Graduated', 'Profession',␣
↪'Spending_Score', 'Var_1']

15
# Créer les graphiques pour chaque feature
for feature in categorical_features:
# Calculer les distributions
train_dist = train_df[feature].value_counts(normalize=True).reset_index()
train_dist.columns = [feature, 'Train']
test_dist = test_df[feature].value_counts(normalize=True).reset_index()
test_dist.columns = [feature, 'Test']

# Merger et convertir en pourcentages


df_dist = train_dist.merge(test_dist, on=feature, how='outer').fillna(0)
df_dist['Train'] *= 100
df_dist['Test'] *= 100

# Créer le graphique
fig = [Link]()
fig.add_trace([Link](x=df_dist[feature], y=df_dist['Train'],
name='Train', marker_color='blue'))
fig.add_trace([Link](x=df_dist[feature], y=df_dist['Test'],
name='Test', marker_color='orange'))

fig.update_layout(
barmode='group',
title=f'{feature}: Train vs Test (%)',
xaxis_title=feature,
yaxis_title='Percentage (%)',
height=500,
xaxis_tickangle=-45 if feature == 'Profession' else 0
)
[Link]()

Stratégie Tester plusieurs approches de complexité croissante pour comprendre les gains apportés
par chaque technique et identifier la meilleure combinaison finale
MODÈLE 1 : LightGBM Simple (Baseline) - Preprocessing minimal : Label encoding uniquement -
Hyperparamètres par défaut de LightGBM - Conservation des valeurs manquantes (gestion native)
Est généralement meilleur que XgBoost sur les petites bases de données
MODÈLE 2 : LightGBM avec Lookback Objectif : Exploiter l’information historique pour les IDs
connus communs entre le test et le train Vu que 88% des IDs du test sont déjà présents dans le
training si la segementation ne change pas alors on pourrait s’attendre à autant d’accuracy sur le
dataframe de test. SUr les Ids inconnus on apllique in lgbm normal sans les iDS pour features.
MODÈLE 3 : LightGBM Optimisé (Optuna) Objectif : Maximiser les performances LightGBM via
tuning Optimisation Optuna avec 100-200 trials Hyperparamètres optimisés : - num_leaves: [20,
50] - max_depth: [5, 15] - learning_rate: [0.01, 0.1] - min_child_samples: [20, 100] - subsample:
[0.6, 1.0] - colsample_bytree: [0.6, 1.0] - lambda_l1, lambda_l2: [0, 10]
MODÈLE 4 : Régression Logistique (Sans NaN) Objectif : Tester un modèle linéaire simple comme
contraste Caractéristiques : Suppression de toutes les lignes avec valeurs manquantes (~17% de

16
perte) One-hot encoding des catégorielles (passage à ~25 features)
MODÈLE 5 : SVM (Sans NaN) Objectif : Tester un modèle non-linéaire sans arbres Caractéris-
tiques : Suppression des NaN (~17% de perte) One-hot encoding + standardisation SVM avec
kernel RBF (non-linéaire) Grid search pour optimiser C et gamma
MODELE 6 : ENSEMBLE VOTING pour prendre le meilleur des modèles mentionnés

17

Vous aimerez peut-être aussi