Combler les lacunes : Un guide comparatif des techniques d’imputation en Machine Learning

Dans notre précédente analyse des modèles de régression pénalisée (comme LassoRidge et ElasticNet), nous avons montré leur efficacité pour gérer la multicolinéarité, permettant d’exploiter un éventail plus large de caractéristiques et d’améliorer les performances des modèles.

Nous abordons aujourd’hui un autre aspect clé du prétraitement des données : la gestion des valeurs manquantes. Ces lacunes peuvent grandement compromettre la précision et la fiabilité des modèles si elles ne sont pas traitées correctement.

Cet article explore différentes stratégies d’imputation pour traiter les données manquantes et les intégrer à notre pipeline. Cette approche nous permettra d’affiner encore notre précision prédictive en réintégrant des caractéristiques précédemment exclues, tirant ainsi pleinement parti de la richesse de notre jeu de données.

Commençons sans plus attendre.

Vue d’ensemble

Cet article est divisé en trois parties :

  1. Reconstruction de l’imputation manuelle avec SimpleImputer
  2. Perfectionnement des techniques d’imputation avec IterativeImputer
  3. Exploitation des relations de voisinage via l’imputation KNN

Reconstruction de l’imputation manuelle avec SimpleImputer

Dans cette première partie, nous revisitons et automatisons nos techniques d’imputation manuelle en utilisant SimpleImputer.

Lors de notre précédente analyse du jeu de données Ames Housing, nous avions exploré des stratégies manuelles adaptées à chaque type de donnée :

  • Variables catégorielles (ex. : PoolQC) : Les valeurs manquantes indiquaient souvent l’absence d’une caractéristique (ici, l’absence de piscine). Nous les avons remplacées par "None" pour préserver l’intégrité des données.
  • Variables numériques : Imputation par la moyenne ou d’autres méthodes statistiques.

Désormais, avec SimpleImputer de scikit-learn, nous automatisons ces processus pour gagner en :

  • Reproductibilité
  • Efficacité

# Import the necessary libraries import pandas as pd from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer from sklearn.linear_model import Lasso, Ridge, ElasticNet from sklearn.model_selection import cross_val_score # Load the dataset Ames = pd.read_csv(‘Ames.csv’) # Exclude ‘PID’ and ‘SalePrice’ from features and specifically handle the ‘Electrical’ column numeric_features = Ames.select_dtypes(include=[‘int64’, ‘float64’]).drop(columns=[‘PID’, ‘SalePrice’]).columns categorical_features = Ames.select_dtypes(include=[‘object’]).columns.difference([‘Electrical’]) electrical_feature = [‘Electrical’] # Specifically handle the ‘Electrical’ column # Helper function to fill ‘None’ for missing categorical data def fill_none(X): return X.fillna(« None ») # Pipeline for numeric features: Impute missing values then scale numeric_transformer = Pipeline(steps=[ (‘impute_mean’, SimpleImputer(strategy=’mean’)), (‘scaler’, StandardScaler()) ]) # Pipeline for general categorical features: Fill missing values with ‘None’ then apply one-hot encoding categorical_transformer = Pipeline(steps=[ (‘fill_none’, FunctionTransformer(fill_none, validate=False)), (‘onehot’, OneHotEncoder(handle_unknown=’ignore’)) ]) # Specific transformer for ‘Electrical’ using the mode for imputation electrical_transformer = Pipeline(steps=[ (‘impute_electrical’, SimpleImputer(strategy=’most_frequent’)), (‘onehot_electrical’, OneHotEncoder(handle_unknown=’ignore’)) ]) # Combined preprocessor for numeric, general categorical, and electrical data preprocessor = ColumnTransformer( transformers=[ (‘num’, numeric_transformer, numeric_features), (‘cat’, categorical_transformer, categorical_features), (‘electrical’, electrical_transformer, electrical_feature) ]) # Target variable y = Ames[‘SalePrice’] # All features X = Ames[numeric_features.tolist() + categorical_features.tolist() + electrical_feature] # Define the model pipelines with preprocessor and regressor models = { ‘Lasso’: Lasso(max_iter=20000), ‘Ridge’: Ridge(), ‘ElasticNet’: ElasticNet() } results = {} for name, model in models.items(): pipeline = Pipeline(steps=[ (‘preprocessor’, preprocessor), (‘regressor’, model) ]) # Perform cross-validation scores = cross_val_score(pipeline, X, y) results[name] = round(scores.mean(), 4) # Output the cross-validation scores print(« Cross-validation scores with Simple Imputer: », results)

Les résultats de cette implémentation sont présentés ci-dessous, illustrant comment l’imputation simple influence la précision du modèle et établit une référence pour les méthodes plus sophistiquées abordées ultérieurement :

Cross-validation scores with Simple Imputer: {‘Lasso’: 0.9138, ‘Ridge’: 0.9134, ‘ElasticNet’: 0.8752}

Le remplacement des méthodes manuelles par une approche pipeline améliore significativement le traitement des données :

  1. Efficacité et réduction des erreurs
    • L’imputation manuelle est chronophage et sujette aux erreurs, surtout avec des données complexes.
    • Le pipeline automatise ces étapes, garantissant des transformations cohérentes et minimisant les risques d’erreurs.
  2. Réutilisabilité et intégration
    • Les méthodes manuelles sont peu réutilisables.
    • Les pipelines intègrent l’ensemble du prétraitement et de la modélisation, les rendant facilement réutilisables et compatibles avec l’entraînement des modèles.
  3. Prévention des fuites de données
    • L’imputation manuelle peut involontairement inclure des données de test lors du calcul des valeurs (ex. : moyenne calculée sur l’ensemble du dataset).
    • Les pipelines éliminent ce risque via la méthodologie fit/transform, en s’appuyant uniquement sur le jeu d’entraînement.

Flexibilité démontrée avec SimpleImputer

Cette structure, illustrée ici avec SimpleImputer, offre une approche modulable pour le prétraitement, adaptable à diverses stratégies d’imputation. Dans les sections suivantes, nous explorerons des techniques plus avancées et leur impact sur les performances des modèles.

2. Perfectionnement des techniques d’imputation avec IterativeImputer

Dans cette deuxième partie, nous testons IterativeImputer, une méthode d’imputation plus sophistiquée qui modélise chaque caractéristique avec valeurs manquantes comme une fonction des autres caractéristiques (approche round-robin).

Contraste avec les méthodes simples

  • Les approches basiques (ex. : moyenne/médiane) appliquent une statistique globale.
  • IterativeImputer utilise une régression : chaque caractéristique incomplète devient une variable dépendante prédite par les autres.

# Import the necessary libraries import pandas as pd from sklearn.pipeline import Pipeline from sklearn.experimental import enable_iterative_imputer # This line is needed for IterativeImputer from sklearn.impute import SimpleImputer, IterativeImputer from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer from sklearn.linear_model import Lasso, Ridge, ElasticNet from sklearn.model_selection import cross_val_score # Load the dataset Ames = pd.read_csv(‘Ames.csv’) # Exclude ‘PID’ and ‘SalePrice’ from features and specifically handle the ‘Electrical’ column numeric_features = Ames.select_dtypes(include=[‘int64’, ‘float64’]).drop(columns=[‘PID’, ‘SalePrice’]).columns categorical_features = Ames.select_dtypes(include=[‘object’]).columns.difference([‘Electrical’]) electrical_feature = [‘Electrical’] # Specifically handle the ‘Electrical’ column # Helper function to fill ‘None’ for missing categorical data def fill_none(X): return X.fillna(« None ») # Pipeline for numeric features: Iterative imputation then scale numeric_transformer_advanced = Pipeline(steps=[ (‘impute_iterative’, IterativeImputer(random_state=42)), (‘scaler’, StandardScaler()) ]) # Pipeline for general categorical features: Fill missing values with ‘None’ then apply one-hot encoding categorical_transformer = Pipeline(steps=[ (‘fill_none’, FunctionTransformer(fill_none, validate=False)), (‘onehot’, OneHotEncoder(handle_unknown=’ignore’)) ]) # Specific transformer for ‘Electrical’ using the mode for imputation electrical_transformer = Pipeline(steps=[ (‘impute_electrical’, SimpleImputer(strategy=’most_frequent’)), (‘onehot_electrical’, OneHotEncoder(handle_unknown=’ignore’)) ]) # Combined preprocessor for numeric, general categorical, and electrical data preprocessor_advanced = ColumnTransformer( transformers=[ (‘num’, numeric_transformer_advanced, numeric_features), (‘cat’, categorical_transformer, categorical_features), (‘electrical’, electrical_transformer, electrical_feature) ]) # Target variable y = Ames[‘SalePrice’] # All features X = Ames[numeric_features.tolist() + categorical_features.tolist() + electrical_feature] # Define the model pipelines with preprocessor and regressor models = { ‘Lasso’: Lasso(max_iter=20000), ‘Ridge’: Ridge(), ‘ElasticNet’: ElasticNet() } results_advanced = {} for name, model in models.items(): pipeline = Pipeline(steps=[ (‘preprocessor’, preprocessor_advanced), (‘regressor’, model) ]) # Perform cross-validation scores = cross_val_score(pipeline, X, y) results_advanced[name] = round(scores.mean(), 4) # Output the cross-validation scores for advanced imputation print(« Cross-validation scores with Iterative Imputer: », results_advanced)

Bien que les gains de précision apportés par IterativeImputer par rapport à SimpleImputer soient modestes, ils soulignent un aspect crucial de l’imputation de données : la complexité et les interdépendances présentes dans un jeu de données ne garantissent pas systématiquement une amélioration spectaculaire des performances, même avec des méthodes plus sophistiquées.

Cross-validation scores with Iterative Imputer: {‘Lasso’: 0.9142, ‘Ridge’: 0.9135, ‘ElasticNet’: 0.8746}

Exploitation des relations de voisinage avec l’imputation KNN

Dans cette dernière partie, nous explorons KNNImputer, une méthode qui impute les valeurs manquantes en utilisant la moyenne des k-plus proches voisins identifiés dans l’ensemble d’entraînement.

Fondement théorique

Cette approche repose sur l’hypothèse que des données similaires se trouvent à proximité dans l’espace des caractéristiques. Elle s’avère particulièrement efficace pour les jeux de données où cette hypothèse est vérifiée.

Cas d’usage privilégiés

L’imputation KNN est puissante dans les scénarios où :

  • Les points de données partageant des caractéristiques similaires
  • Sont susceptibles d’avoir des réponses ou attributs similaires

# Import the necessary libraries import pandas as pd from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer, KNNImputer from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer from sklearn.linear_model import Lasso, Ridge, ElasticNet from sklearn.model_selection import cross_val_score # Load the dataset Ames = pd.read_csv(‘Ames.csv’) # Exclude ‘PID’ and ‘SalePrice’ from features and specifically handle the ‘Electrical’ column numeric_features = Ames.select_dtypes(include=[‘int64’, ‘float64’]).drop(columns=[‘PID’, ‘SalePrice’]).columns categorical_features = Ames.select_dtypes(include=[‘object’]).columns.difference([‘Electrical’]) electrical_feature = [‘Electrical’] # Specifically handle the ‘Electrical’ column # Helper function to fill ‘None’ for missing categorical data def fill_none(X): return X.fillna(« None ») # Pipeline for numeric features: K-Nearest Neighbors Imputation then scale numeric_transformer_knn = Pipeline(steps=[ (‘impute_knn’, KNNImputer(n_neighbors=5)), (‘scaler’, StandardScaler()) ]) # Pipeline for general categorical features: Fill missing values with ‘None’ then apply one-hot encoding categorical_transformer = Pipeline(steps=[ (‘fill_none’, FunctionTransformer(fill_none, validate=False)), (‘onehot’, OneHotEncoder(handle_unknown=’ignore’)) ]) # Specific transformer for ‘Electrical’ using the mode for imputation electrical_transformer = Pipeline(steps=[ (‘impute_electrical’, SimpleImputer(strategy=’most_frequent’)), (‘onehot_electrical’, OneHotEncoder(handle_unknown=’ignore’)) ]) # Combined preprocessor for numeric, general categorical, and electrical data preprocessor_knn = ColumnTransformer( transformers=[ (‘num’, numeric_transformer_knn, numeric_features), (‘cat’, categorical_transformer, categorical_features), (‘electrical’, electrical_transformer, electrical_feature) ]) # Target variable y = Ames[‘SalePrice’] # All features X = Ames[numeric_features.tolist() + categorical_features.tolist() + electrical_feature] # Define the model pipelines with preprocessor and regressor models = { ‘Lasso’: Lasso(max_iter=20000), ‘Ridge’: Ridge(), ‘ElasticNet’: ElasticNet() } results_knn = {} for name, model in models.items(): pipeline = Pipeline(steps=[ (‘preprocessor’, preprocessor_knn), (‘regressor’, model) ]) # Perform cross-validation scores = cross_val_score(pipeline, X, y) results_knn[name] = round(scores.mean(), 4) # Output the cross-validation scores for KNN imputation print(« Cross-validation scores with KNN Imputer: », results_knn)

-Les résultats obtenus avec KNNImputer montrent une amélioration très légère par rapport à ceux de SimpleImputer et IterativeImputer :

Cross-validation scores with KNN Imputer: {‘Lasso’: 0.9146, ‘Ridge’: 0.9138, ‘ElasticNet’: 0.8748}

Cette légère amélioration suggère que, pour certains jeux de données :

  1. L’approche basée sur la proximité de KNNImputer
  2. Qui prend en compte la similarité entre points de données

Peut s’avérer plus efficace pour :

  • Capturer la structure sous-jacente des données
  • Préserver les relations intrinsèques
  • Potentiellement améliorer la précision des prédictions

Récupéré de : https://machinelearningmastery.com/filling-the-gaps-a-comparative-guide-to-imputation-techniques-in-machine-learning/

Auteur: Vinod Chugani