Régression (v1)#

Import des outils / jeu de données#

 1import statistics
 2
 3import matplotlib.pyplot as plt
 4import numpy as np
 5import pandas as pd
 6import seaborn as sns
 7import statsmodels.api as sm
 8import statsmodels.stats.api as sms
 9import xgboost
10from keras import layers
11from scipy import stats
12from scipy.stats import kstest, pearsonr, poisson
13from sklearn.cross_decomposition import PLSRegression
14from sklearn.linear_model import LinearRegression, PoissonRegressor
15from sklearn.metrics import mean_absolute_error, mean_squared_error
16from sklearn.model_selection import train_test_split
17from sklearn.preprocessing import PolynomialFeatures
18from statsmodels.compat import lzip
19from statsmodels.graphics.regressionplots import *
20from statsmodels.stats.outliers_influence import variance_inflation_factor
21from tensorflow import keras
22
23from src.config import data_folder
24from src.constants import var_categoriques, var_numeriques
25from src.utils import init_notebook
2023-12-17 21:40:17.874455: I external/local_tsl/tsl/cuda/cudart_stub.cc:31] Could not find cuda drivers on your machine, GPU will not be used.
2023-12-17 21:40:17.904062: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:9261] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2023-12-17 21:40:17.904097: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:607] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2023-12-17 21:40:17.905286: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1515] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2023-12-17 21:40:17.911163: I external/local_tsl/tsl/cuda/cudart_stub.cc:31] Could not find cuda drivers on your machine, GPU will not be used.
2023-12-17 21:40:17.912274: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2023-12-17 21:40:21.024366: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
1init_notebook()
1df = pd.read_csv(
2    f"{data_folder}/data-cleaned-feature-engineering.csv",
3    sep=",",
4    index_col="ID",
5    parse_dates=True,
6)
1df_transforme = pd.read_csv(
2    f"{data_folder}/data-transformed.csv",
3    sep=",",
4    index_col="ID",
5    parse_dates=True,
6)

Variables globales#

Fonctions et variables utiles#

1score_modeles = []
1def affiche_score(modele, y_test, y_pred):
2    """Affiche la MSE, RMSE et MAE du modèle."""
3    print(f"MSE = {mean_squared_error(y_test, y_pred)}")
4    print(f"RMSE = {mean_squared_error(y_test, y_pred, squared=False)}")
5    print(f"MAE = {mean_absolute_error(y_test, y_pred)}")
1def ajout_score(modele, nom_modele, y_test, y_pred):
2    """Ajoute la MMSE, RMSE et MAE au dataframe score_modeles."""
3    score_modeles.extend(
4        (
5            [nom_modele, "mse", mean_squared_error(y_test, y_pred)],
6            [nom_modele, "rmse", mean_squared_error(y_test, y_pred, squared=False)],
7            [nom_modele, "mae", mean_absolute_error(y_test, y_pred)],
8        )
9    )

Régression linéaire simple#

Modèle simple : une variable à expliquer $Y$ et une seule variable explicative $X$.

$$y_i = \beta_0 + \beta_1 X_i + \epsilon_i$$

Hypothèses à vérifier pour la régression linéaire simple :

  1. il existe une corrélation linéaire entre $X$ et $Y$

  2. la distribution de l’erreur $\epsilon$ est indépendante de la variable X (exogénéité)

  3. l’erreur suit une loi normale centrée i.e. $E(ε_i) = 0$

  4. l’erreur est de variance constante (homoscédasticité) i.e $Var(ε_i) = \sigma^2$, une constante

  5. les erreurs sont indépendantes (absence d’autocorrélation) i.e. $Cov(εi, εj) = 0$, pour tout $i, j \in N \times N, i \neq j$

1X = np.array(df_transforme["Income"])
2Y = np.array(df_transforme["NumStorePurchases"])

Hypothèse 1 : corrélation linéaire#

1print(
2    "Le coefficient de corrélation linéaire entre X et Y vaut",
3    pearsonr(X, Y)[0],
4    "la pvalue associée vaut",
5    pearsonr(X, Y)[1],
6    "il existe donc bien une relation linéaire entre X et Y.",
7)
Le coefficient de corrélation linéaire entre X et Y vaut 0.6790710752806047 la pvalue associée vaut 1.3161539637560999e-276 il existe donc bien une relation linéaire entre X et Y.

Le reste des hypothèses à tester requiert d’effectuer la régression linéaire :

1X_train, X_test, Y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=0)
1model = LinearRegression().fit(X_train.reshape(-1, 1), Y_train)
2Y_train_hat = model.predict(X_train.reshape(-1, 1))
1plt.scatter(X_train, Y_train, color="black")
2plt.plot(X_train, Y_train_hat, color="red")
3plt.title("Régression linéaire du nombre d'achats en magasin en fonction du revenu")
Text(0.5, 1.0, "Régression linéaire du nombre d'achats en magasin en fonction du revenu")
../_images/f31f3f493fc7b4dc12bae4510fb38d6caf9febb8361e607ed742c12c18ade3d6.png
1print(model.intercept_, model.coef_, model.score(X_train.reshape(-1, 1), Y_train))
0.41932027328891675 [2.21369643] 0.47029950823152755

Equation de régression :

$$y_i = 0.42 + 2.21 * x_i$$

Hypothèse 2 : exogénéité#

1residuals = Y_train - Y_train_hat
2
3plt.scatter(X_train, residuals)
<matplotlib.collections.PathCollection at 0x7f95055e3b50>
../_images/562f5d19ec70f12084fa9067093089b8041be31b5f056b61ce4b5e8225bd2add.png
1print(
2    "Le coefficient de corrélation entre X et les residus vaut",
3    pearsonr(X_train, residuals)[0],
4    ". On a bien indépendance et donc exogénéité.",
5)
Le coefficient de corrélation entre X et les residus vaut -2.8216361538935253e-16 . On a bien indépendance et donc exogénéité.

Hypothèse 3 : l’erreur suit une loi normale centrée i.e. E(ε_i) = 0#

1plt.hist(residuals, density=True)
2
3x = np.linspace(-7, 7, 100)
4plt.plot(x, stats.norm.pdf(x, 0, 1))
5plt.title("Histogramme des résidus en superposition avec la densité de la loi normale")
6plt.show()
../_images/01612617e642dd3da6fb0a21686393df73e282f5ca1c677d52290ba974879cde.png
1print("La moyenne des résidus vaut", statistics.mean(residuals))
2print("Mais les résidus ne suivent pas une loi normale")
La moyenne des résidus vaut -5.35770830037629e-16
Mais les résidus ne suivent pas une loi normale
1sm.qqplot(residuals, line="45")
2print("On constate sur le qqplot que les points ne suivent pas la droite x = y")
On constate sur le qqplot que les points ne suivent pas la droite x = y
../_images/5bc22217ad5c3e7d75a666c137b5abde73e08b0727a6a9265376dbe945e64488.png
1print(
2    "Un test de shapiro, pour tester l'hypothèse de normalité, nous donne une pvalue de",
3    stats.shapiro(residuals)[1],
4    ". On rejette l'hypothèse nulle et on conclut que les résidus ne suivent pas une loi normale.",
5)
Un test de shapiro, pour tester l'hypothèse de normalité, nous donne une pvalue de 1.5231739480814355e-12 . On rejette l'hypothèse nulle et on conclut que les résidus ne suivent pas une loi normale.

Hypothèse 4 : homoscédasticité#

1def abline(slope, intercept):
2    axes = plt.gca()
3    x_vals = np.array(axes.get_xlim())
4    y_vals = intercept + slope * x_vals
5    plt.plot(x_vals, y_vals, "-", color="red")
1plt.plot(residuals, "bo")
2plt.title("Nuage de points des résidus")
3abline(0, 7)
4abline(0, -7)
../_images/5a02a42e1c479e1e4e304c5abdfacab2a3734c44060d28305c261b5d64e14daa.png
1fit = sm.OLS(Y_train, sm.add_constant(X_train)).fit()
1fit.summary()
OLS Regression Results
Dep. Variable: y R-squared: 0.470
Model: OLS Adj. R-squared: 0.470
Method: Least Squares F-statistic: 1451.
Date: Sun, 17 Dec 2023 Prob (F-statistic): 9.81e-228
Time: 21:40:24 Log-Likelihood: -3726.4
No. Observations: 1636 AIC: 7457.
Df Residuals: 1634 BIC: 7468.
Df Model: 1
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
const 0.4193 0.153 2.738 0.006 0.119 0.720
x1 2.2137 0.058 38.089 0.000 2.100 2.328
Omnibus: 51.217 Durbin-Watson: 2.019
Prob(Omnibus): 0.000 Jarque-Bera (JB): 55.343
Skew: 0.447 Prob(JB): 9.60e-13
Kurtosis: 3.105 Cond. No. 7.78


Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
1res_names = ["F statistic", "p-value"]
2gq_test = sms.het_goldfeldquandt(fit.resid, fit.model.exog)
3lzip(res_names, gq_test)
[('F statistic', 1.0633264046598987), ('p-value', 0.19033564154336408)]
1bp_test = sms.het_breuschpagan(fit.resid, fit.model.exog)
2lzip(res_names, bp_test)
[('F statistic', 222.40463115768435), ('p-value', 2.7032416707307303e-50)]

Le test de Goldfeld-Quandt ne nous donne pas d’hétéroscédasticité, contraiement au test de Breusch-Pagan.

Hypothèse 5 : absence d’autocorrélation#

1sm.graphics.tsa.plot_acf(residuals)
2print("On remarque une absence d'autocorrélation.")
On remarque une absence d'autocorrélation.
../_images/0fdb17f43232e8d2e2e893575ab637b7b9a6bc457c1c29bf3b82d3b48a967fe9.png

Distance de Cook#

1influence = fit.get_influence()
2cooks = influence.cooks_distance
1plt.scatter(X_train, cooks[0])
2plt.xlabel("Revenus")
3plt.ylabel("Distances de Cook")
4plt.show()
../_images/c189c9766bb36f9572312b62fa67ee048b61403167a071b4a6ececaa9df0f00d.png
1cooks_indexes = [i for i, x in enumerate(cooks[0] > 0.010) if x]
2print(cooks_indexes)
3## affiche les indexes des individus dont les distances de cook dépassent une certaine valeur
[532, 1026]
1influence_plot(fit)
2print("")

../_images/75095ee8ff6bd716f3dc2a1d103c1fe70c46a701694998dc18f68d9be19d913f.png

Score du modèle#

Qualité d’ajustement#

1print(f"Le R² du modèle vaut {model.score(X_train.reshape(-1, 1), Y_train)}")
Le R² du modèle vaut 0.47029950823152755
1print(f"MSE = {mean_squared_error(Y_train, Y_train_hat)}")
2print(f"RMSE = {mean_squared_error(Y_train, Y_train_hat, squared=False)}")
3print(f"MAE = {mean_absolute_error(Y_train, Y_train_hat)}")
MSE = 5.571127360403869
RMSE = 2.3603235711240673
MAE = 1.8519616230655271

Qualité de prédiction#

Train / test split

1print(
2    f"R² du modèle sur les données d'entraînement = {model.score(X_train.reshape(-1, 1), Y_train)}"
3)
4print(
5    f"R² du modèle sur les données de test = {model.score(X_test.reshape(-1, 1), y_test)}"
6)
R² du modèle sur les données d'entraînement = 0.47029950823152755
R² du modèle sur les données de test = 0.4225134353582901
1y_pred = model.predict(X_test.reshape(-1, 1))
1plt.scatter(X_test, y_test, color="black")
2plt.plot(X_test, y_pred, color="red")
3plt.title("Nombre d'achats en magasin en fonction du revenu : données test")
Text(0.5, 1.0, "Nombre d'achats en magasin en fonction du revenu : données test")
../_images/1cda7e523f53743c9fd691fb1ad878b4e064f7e918004a508e4bc02dd050aa34.png
1affiche_score(model, y_test, y_pred)
MSE = 5.853872571999743
RMSE = 2.419477747779413
MAE = 1.8268826341039772
1nom_modele = "Régression simple"
2ajout_score(model, nom_modele, y_test, y_pred)
1## todo: cross-validation

Régression linéaire multiple#

Modèle multiple : une variable à expliquer $Y$ et $P$ variables explicatives $X_i$

$$y_i = \beta_0 + \beta_1 X_i(1) + \beta_2 X_i(2) + \beta_3 X_i(3) + … + \beta_P X_i(P) + \epsilon_i$$

Hypothèses à vérifier pour la régression linéaire multiple :

  1. il existe une corrélation linéaire entre $X_i$ et $Y$

  2. $Cov(X_i, ε_j) = 0$, pour tout $i, j \in N \times N, i \neq j$ (exogénéité) et pour chaque variable explicative $X_p$

  3. l’erreur suit une loi normale centrée i.e. $E(ε_i) = 0$

  4. l’erreur est de variance constante (homoscédasticité) i.e $Var(ε_i) = \sigma^2$, une constante

  5. les erreurs sont indépendantes (absence d’autocorrélation) i.e. $Cov(εi, εj) = 0$, pour tout $i, j \in N \times N, i \neq j$

  6. absence de colinéarité entre les variables explicatives, i.e. $X^t * X$ est régulière, $det(X^t * X) \neq 0$

Pour les besoins de la régression linéaire, nous créons des variables muettes (dummy variables) pour inclure les variables catégoriques Education et Marital_status à la régression.

1df_reg = df_transforme.copy()
1df_reg = pd.get_dummies(df_reg, columns=["Education", "Marital_Status"])
1df_reg.columns
Index(['Year_Birth', 'Income', 'Kidhome', 'Teenhome', 'Dt_Customer', 'Recency',
       'MntWines', 'MntFruits', 'MntMeatProducts', 'MntFishProducts',
       'MntSweetProducts', 'MntGoldProds', 'NumDealsPurchases',
       'NumWebPurchases', 'NumCatalogPurchases', 'NumStorePurchases',
       'NumWebVisitsMonth', 'AcceptedCmp3', 'AcceptedCmp4', 'AcceptedCmp5',
       'AcceptedCmp1', 'AcceptedCmp2', 'Complain', 'Response',
       'NbAcceptedCampaigns', 'HasAcceptedCampaigns', 'NbChildren',
       'Education_2n Cycle', 'Education_Basic', 'Education_Graduation',
       'Education_Master', 'Education_PhD', 'Marital_Status_Divorced',
       'Marital_Status_Married', 'Marital_Status_Single',
       'Marital_Status_Together', 'Marital_Status_Widow'],
      dtype='object')

Pour cette régression, on suppose que l’entreprise veut profiler au mieux les clients qui achètent dans le magasin. C’est pourquoi la variable d’intérêt pour la régression sera le nombre d'achats en magasin.

Nos variables explicatives sont des variables numériques et des variables catégoriques transformées en variables muettes. Nous commençons par un grand nombre de variables explicatives puis nous en éliminerons progressivement pendant la vérifications des hypothèses.

 1var_numeriques_reg = [
 2    "Income",
 3    "Year_Birth",
 4    "Recency",
 5    "NbAcceptedCampaigns",
 6    "NumWebPurchases",
 7    "NumDealsPurchases",
 8    "NumWebVisitsMonth",
 9    "NumCatalogPurchases",
10    "MntWines",
11    "MntFruits",
12    "MntMeatProducts",
13    "MntFishProducts",
14    "MntSweetProducts",
15    "MntGoldProds",
16]
17
18var_categoriques_reg = [
19    "NbChildren",
20    "Education_2n Cycle",
21    "Education_Basic",
22    "Education_Graduation",
23    "Education_Master",
24    "Education_PhD",
25    "Marital_Status_Divorced",
26    "Marital_Status_Married",
27    "Marital_Status_Single",
28    "Marital_Status_Together",
29    "Marital_Status_Widow",
30]
1X = df_reg[var_categoriques_reg + var_numeriques_reg]
2Y = df_reg["NumStorePurchases"]
1print(
2    "Nous avons",
3    X.shape[1],
4    "variables explicatives au début de la régression dont",
5    X[var_numeriques_reg].shape[1],
6    "variables numériques.",
7)
Nous avons 25 variables explicatives au début de la régression dont 14 variables numériques.

Hypothèse 1 : lien linéaire entre Y et les variables explicatives numériques#

1sns.pairplot(df_transforme, x_vars=var_numeriques_reg[0:4], y_vars="NumStorePurchases")
2sns.pairplot(df_transforme, x_vars=var_numeriques_reg[4:8], y_vars="NumStorePurchases")
3sns.pairplot(df_transforme, x_vars=var_numeriques_reg[8:12], y_vars="NumStorePurchases")
4sns.pairplot(df_transforme, x_vars=var_numeriques_reg[12:], y_vars="NumStorePurchases")
<seaborn.axisgrid.PairGrid at 0x7f94fd9e1f30>
../_images/a602bceb59703d529221a34e26f696644b66b63fe4943f714fbb95abe8a31c7a.png ../_images/f44d836d98f8477bb7ab618fe93b8b239986fc18b5bb9513d09d90b21080776b.png ../_images/179f4a7b465df81f6bc4bede025297cc10cfd8ff7016a74ad510c07f766bfbfa.png ../_images/025fc97c2039d0e02b3284a2e32c79edd9ffcc15b004015732b3c3c3220d4e79.png
 1for x in X[var_numeriques_reg].columns:
 2    corr = pearsonr(X[x], Y)
 3    print(
 4        "Le coefficient de corrélation linéaire entre Y et",
 5        x,
 6        "vaut",
 7        corr[0],
 8        "la pvalue vaut",
 9        corr[1],
10    )
Le coefficient de corrélation linéaire entre Y et Income vaut 0.6790710752806047 la pvalue vaut 1.3161539637560999e-276
Le coefficient de corrélation linéaire entre Y et Year_Birth vaut -0.14526120372623744 la pvalue vaut 4.1123238450486986e-11
Le coefficient de corrélation linéaire entre Y et Recency vaut 0.001463326219470877 la pvalue vaut 0.9472714287541293
Le coefficient de corrélation linéaire entre Y et NbAcceptedCampaigns vaut 0.21234097582784053 la pvalue vaut 2.8026864212886505e-22
Le coefficient de corrélation linéaire entre Y et NumWebPurchases vaut 0.4868594514010033 la pvalue vaut 3.457837889457746e-122
Le coefficient de corrélation linéaire entre Y et NumDealsPurchases vaut 0.07208375232131894 la pvalue vaut 0.0011062951780020662
Le coefficient de corrélation linéaire entre Y et NumWebVisitsMonth vaut -0.43919311950774226 la pvalue vaut 3.425052472177628e-97
Le coefficient de corrélation linéaire entre Y et NumCatalogPurchases vaut 0.5627033023451984 la pvalue vaut 3.924437146611002e-171
Le coefficient de corrélation linéaire entre Y et MntWines vaut 0.7427337091436748 la pvalue vaut 0.0
Le coefficient de corrélation linéaire entre Y et MntFruits vaut 0.5313945643467054 la pvalue vaut 2.074678857407921e-149
Le coefficient de corrélation linéaire entre Y et MntMeatProducts vaut 0.7101686508975804 la pvalue vaut 1.06156040464e-313
Le coefficient de corrélation linéaire entre Y et MntFishProducts vaut 0.5313511939986364 la pvalue vaut 2.215537485896551e-149
Le coefficient de corrélation linéaire entre Y et MntSweetProducts vaut 0.5374817998598812 la pvalue vaut 1.8679386261982883e-153
Le coefficient de corrélation linéaire entre Y et MntGoldProds vaut 0.46958362271100534 la pvalue vaut 1.1467326528065478e-112
1## TODO: faire un joli affichage avec SNS heatmap

On décide déjà d’écarter la variable Recency de la régression.

1del X["Recency"]
2var_numeriques_reg.remove("Recency")

Réalisation du modèle#

1X_train, X_test, Y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=0)
1model_multiple = LinearRegression().fit(X_train, Y_train)
2Y_train_hat = model_multiple.predict(X_train)
1plt.scatter(Y_train, Y_train_hat, color="black")
2plt.title("Valeurs prédites contres vraies valeurs (donnéees d'entraînement)")
3abline(1, 0)
../_images/e993df87a23cbad84a06614c5c658b3ca03065cb2f7fea5bbb543d12dd71763f.png

Hypothèse 2 : exogénéité sur les variables numériques#

(Cela n’a pas de sens de tester l’exogénéité sur les variables catégoriques car la notion de corrélation ne fonctionne pas avec celles-ci.)

1residuals = Y_train - Y_train_hat
 1for x in X_train[var_numeriques_reg].columns:
 2    corr = pearsonr(X_train[x], residuals)
 3    print(
 4        "Le coefficient de corrélation entre",
 5        x,
 6        "et les residus vaut",
 7        corr[0],
 8        "et la pvalue associée vaut",
 9        corr[1],
10    )
Le coefficient de corrélation entre Income et les residus vaut -5.2827750854356204e-17 et la pvalue associée vaut 0.9999999999999989
Le coefficient de corrélation entre Year_Birth et les residus vaut 2.2936296958930846e-16 et la pvalue associée vaut 0.9999999999999919
Le coefficient de corrélation entre NbAcceptedCampaigns et les residus vaut -1.288574281999022e-16 et la pvalue associée vaut 0.9999999999999919
Le coefficient de corrélation entre NumWebPurchases et les residus vaut 1.1798830142073502e-16 et la pvalue associée vaut 0.9999999999999919
Le coefficient de corrélation entre NumDealsPurchases et les residus vaut 1.285592726024687e-16 et la pvalue associée vaut 0.9999999999999919
Le coefficient de corrélation entre NumWebVisitsMonth et les residus vaut 1.140038584368508e-16 et la pvalue associée vaut 0.9999999999999919
Le coefficient de corrélation entre NumCatalogPurchases et les residus vaut -1.060078674147702e-16 et la pvalue associée vaut 0.9999999999999989
Le coefficient de corrélation entre MntWines et les residus vaut -4.4289658746032856e-17 et la pvalue associée vaut 0.9999999999999989
Le coefficient de corrélation entre MntFruits et les residus vaut 1.42409955355971e-16 et la pvalue associée vaut 0.9999999999999919
Le coefficient de corrélation entre MntMeatProducts et les residus vaut 8.355132991716419e-18 et la pvalue associée vaut 0.9999999999999989
Le coefficient de corrélation entre MntFishProducts et les residus vaut 9.23198149871407e-17 et la pvalue associée vaut 0.9999999999999989
Le coefficient de corrélation entre MntSweetProducts et les residus vaut 1.5135462327897642e-16 et la pvalue associée vaut 0.9999999999999919
Le coefficient de corrélation entre MntGoldProds et les residus vaut 7.925517880869037e-17 et la pvalue associée vaut 0.9999999999999989

Il n’y a pas d’endogénéité.

Hypothèse 3 : l’erreur suit une loi normale centrée i.e. $E(ε_i) = 0$#

1plt.hist(residuals, density=True)
2
3x = np.linspace(-6, 6, 100)
4plt.plot(x, stats.norm.pdf(x, 0, 1))
5plt.title("Histogramme des résidus en superposition avec la densité de la loi normale")
6plt.show()
../_images/3b3fe0d835d342db4c9bec79e2b34be47ac412c05744d13f6af45796de56f3ff.png
1print("La moyenne des résidus vaut", statistics.mean(residuals))
La moyenne des résidus vaut -4.766629905236613e-16
1sm.qqplot(residuals, line="45")
2print("On constate sur le qqplot que les points ne suivent pas la droite x = y")
On constate sur le qqplot que les points ne suivent pas la droite x = y
../_images/e760f309d9cabc6b602342162834ca7a99db6ecf18d6759cd6a6df7ba770e6fb.png
1print(
2    "Un test de shapiro, pour tester l'hypothèse de normalité, nous donne une pvalue de",
3    stats.shapiro(residuals)[1],
4    ". On rejette l'hypothèse nulle et on conclut que les résidus ne suivent pas une loi normale.",
5)
Un test de shapiro, pour tester l'hypothèse de normalité, nous donne une pvalue de 3.973341823017073e-16 . On rejette l'hypothèse nulle et on conclut que les résidus ne suivent pas une loi normale.

Hypothèse 4 : homoscédasticité#

1plt.plot(residuals, "bo")
2plt.title("Nuage de points des résidus")
3abline(0, 6.5)
4abline(0, -6.5)
../_images/9c36afea286160c221a7f3a1a28f5ee345408c68900ad5a7c7a2d6cd3afafbc5.png
1fit = sm.OLS(Y_train, sm.add_constant(X_train)).fit()
1fit.summary()
OLS Regression Results
Dep. Variable: NumStorePurchases R-squared: 0.623
Model: OLS Adj. R-squared: 0.617
Method: Least Squares F-statistic: 120.9
Date: Sun, 17 Dec 2023 Prob (F-statistic): 1.60e-321
Time: 21:40:30 Log-Likelihood: -3449.1
No. Observations: 1636 AIC: 6944.
Df Residuals: 1613 BIC: 7068.
Df Model: 22
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
const 1.1164 0.263 4.252 0.000 0.601 1.632
NbChildren -0.2059 0.099 -2.087 0.037 -0.399 -0.012
Education_2n Cycle 0.4084 0.166 2.455 0.014 0.082 0.735
Education_Basic 0.6801 0.288 2.361 0.018 0.115 1.245
Education_Graduation 0.0099 0.112 0.088 0.930 -0.211 0.230
Education_Master -0.0326 0.139 -0.234 0.815 -0.306 0.240
Education_PhD 0.0506 0.136 0.372 0.710 -0.217 0.318
Marital_Status_Divorced 0.3121 0.150 2.076 0.038 0.017 0.607
Marital_Status_Married 0.3234 0.104 3.103 0.002 0.119 0.528
Marital_Status_Single 0.0786 0.121 0.650 0.516 -0.159 0.316
Marital_Status_Together 0.1935 0.113 1.713 0.087 -0.028 0.415
Marital_Status_Widow 0.2090 0.229 0.911 0.362 -0.241 0.659
Income 0.2647 0.128 2.074 0.038 0.014 0.515
Year_Birth 0.0576 0.055 1.043 0.297 -0.051 0.166
NbAcceptedCampaigns -0.3867 0.084 -4.617 0.000 -0.551 -0.222
NumWebPurchases 0.0437 0.028 1.561 0.119 -0.011 0.099
NumDealsPurchases 0.1072 0.037 2.927 0.003 0.035 0.179
NumWebVisitsMonth -0.1821 0.032 -5.618 0.000 -0.246 -0.119
NumCatalogPurchases -0.1065 0.029 -3.639 0.000 -0.164 -0.049
MntWines 0.4101 0.033 12.471 0.000 0.346 0.475
MntFruits 0.1047 0.055 1.887 0.059 -0.004 0.214
MntMeatProducts 0.1076 0.077 1.407 0.160 -0.042 0.258
MntFishProducts 0.1247 0.048 2.584 0.010 0.030 0.219
MntSweetProducts 0.2224 0.054 4.147 0.000 0.117 0.328
MntGoldProds -0.0399 0.039 -1.034 0.302 -0.116 0.036
Omnibus: 23.189 Durbin-Watson: 2.023
Prob(Omnibus): 0.000 Jarque-Bera (JB): 32.807
Skew: 0.156 Prob(JB): 7.52e-08
Kurtosis: 3.620 Cond. No. 4.03e+16


Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The smallest eigenvalue is 2.16e-28. This might indicate that there are
strong multicollinearity problems or that the design matrix is singular.
1res_names = ["F statistic", "p-value"]
2gq_test = sms.het_goldfeldquandt(fit.resid, fit.model.exog)
3lzip(res_names, gq_test)
[('F statistic', 1.005758558851296), ('p-value', 0.46775077410560034)]
1bp_test = sms.het_breuschpagan(fit.resid, fit.model.exog)
2lzip(res_names, bp_test)
[('F statistic', 482.3119535871289), ('p-value', 7.7911043046153805e-87)]
1white_test = sms.het_white(fit.resid, fit.model.exog)
2lzip(res_names, white_test)
[('F statistic', 792.4970486154685), ('p-value', 1.1109502140244515e-56)]

Le test de Goldfeld-Quandt, qui vérifie la constance de la variance entre deux échantillons, ne nous donne pas d’hétéroscédasticité, contrairement aux tests de White et de Breusch-Pagan.

Hypothèse 5 : absence d’autocorrélation#

1sm.graphics.tsa.plot_acf(residuals)
2print("On remarque une absence d'autocorrélation.")
On remarque une absence d'autocorrélation.
../_images/0dacfe340d3b55bda5283aaccafa85e470bb2f5812ca81faf6d48fbd9573ffda.png

Hypothèse 6 : absence de multicolinéarité#

Calcul de la matrice $X^t \times X$ et de son déterminant :

1X_train_matrix = X_train.to_numpy()
2X_train_matrix_transpose = np.matrix.transpose(X_train_matrix)
1det = np.linalg.det(np.matmul(X_train_matrix_transpose, X_train_matrix))
2det
-3.175207570697781e+53

Variance Inflation Factor#

$$ VIF_i = 1 / ( 1 − R_i^2 ) $$ ​ où $R_i^2 =$ coefficient de détermination non ajusté pour la régression de la ième variable indépendante sur les autres variables restantes ​

Le facteur d’inflation de la variance est une mesure de l’augmentation de la variance des estimations de paramètres si une variable supplémentaire est ajoutée à la régression linéaire. C’est une mesure de la multicolinéarité de la matrice des variables explicatives.

Une recommandation est que si le VIF est supérieur à 10, alors la variable explicative en question est fortement colinéaire avec les autres variables explicatives, et les estimations de paramètres auront de grandes erreurs en raison de cela.

Cf la documentation de statsmodels

1vif = [
2    variance_inflation_factor(sm.add_constant(X_train).values, i)
3    for i in range(sm.add_constant(X_train).shape[1])
4]
5## Rq: la fonction VIF attend une constante dans le dataframe d'entree
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/regression/linear_model.py:1752: RuntimeWarning: divide by zero encountered in scalar divide
  return 1 - self.ssr/self.centered_tss
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/stats/outliers_influence.py:195: RuntimeWarning: divide by zero encountered in scalar divide
  vif = 1. / (1. - r_squared_i)
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/stats/outliers_influence.py:195: RuntimeWarning: divide by zero encountered in scalar divide
  vif = 1. / (1. - r_squared_i)
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/stats/outliers_influence.py:195: RuntimeWarning: divide by zero encountered in scalar divide
  vif = 1. / (1. - r_squared_i)
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/stats/outliers_influence.py:195: RuntimeWarning: divide by zero encountered in scalar divide
  vif = 1. / (1. - r_squared_i)
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/stats/outliers_influence.py:195: RuntimeWarning: divide by zero encountered in scalar divide
  vif = 1. / (1. - r_squared_i)
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/stats/outliers_influence.py:195: RuntimeWarning: divide by zero encountered in scalar divide
  vif = 1. / (1. - r_squared_i)
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/stats/outliers_influence.py:195: RuntimeWarning: divide by zero encountered in scalar divide
  vif = 1. / (1. - r_squared_i)
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/stats/outliers_influence.py:195: RuntimeWarning: divide by zero encountered in scalar divide
  vif = 1. / (1. - r_squared_i)
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/stats/outliers_influence.py:195: RuntimeWarning: divide by zero encountered in scalar divide
  vif = 1. / (1. - r_squared_i)
/home/runner/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/statsmodels/stats/outliers_influence.py:195: RuntimeWarning: divide by zero encountered in scalar divide
  vif = 1. / (1. - r_squared_i)
1print(pd.DataFrame({"VIF": vif[1:]}, index=X_train.columns))
                              VIF
NbChildren               2.275998
Education_2n Cycle            inf
Education_Basic               inf
Education_Graduation          inf
Education_Master              inf
Education_PhD                 inf
Marital_Status_Divorced       inf
Marital_Status_Married        inf
Marital_Status_Single         inf
Marital_Status_Together       inf
Marital_Status_Widow          inf
Income                   6.679390
Year_Birth               1.224293
NbAcceptedCampaigns      1.356774
NumWebPurchases          2.358240
NumDealsPurchases        1.961293
NumWebVisitsMonth        2.516005
NumCatalogPurchases      2.813537
MntWines                 7.709258
MntFruits                2.670042
MntMeatProducts          7.505137
MntFishProducts          2.832624
MntSweetProducts         2.586210
MntGoldProds             1.995179

Réduction du nombre de variables explicatives par le critère AIC#

Le critère d’information d’Akaike s’écrit comme suit :

$$ AIC = 2 \times k - 2 \times \ln(L) $$

où k est le nombre de paramètres à estimer du modèle et L est le maximum de la fonction de vraisemblance du modèle.

 1def aic(X, Y, regressor=""):  ## entrer X le dataframe des variables explicatives,
 2    ## Y la variable expliquée, et le regresseur utilisé
 3    aic_list = []
 4
 5    for col in X.columns:
 6        X_temp = X.drop(col, axis=1)
 7
 8        if regressor == "linearOLS":
 9            model_temp = sm.OLS(Y, sm.add_constant(X_temp)).fit()
10        elif regressor == "GLMpoisson":
11            model_temp = sm.GLM(
12                Y, sm.add_constant(X_temp), family=sm.families.Poisson()
13            ).fit()
14
15        else:
16            return "Entrer un régresseur valide"
17
18        aic_list.append([col, model_temp.aic])
19
20    return aic_list
 1def stepwise(X, Y, regressor=""):  ## entrer X le dataframe des variables explicatives,
 2    ## Y la variable expliquée, et le regresseur utilisé
 3
 4    if regressor == "linearOLS":
 5        current_aic = (
 6            sm.OLS(Y, sm.add_constant(X)).fit().aic
 7        )  ## aic avec les variables actuelles
 8    elif regressor == "GLMpoisson":
 9        current_aic = (
10            sm.GLM(Y, sm.add_constant(X), family=sm.families.Poisson()).fit().aic
11        )
12    else:
13        return "Entrer un régresseur valide"
14
15    aic_list = aic(
16        X, Y, regressor
17    )  ## liste des aic par supression des variables une a une
18
19    my_bool = 0  ## pour indiquer si aucune variable n'est a enlever
20    for i in range(
21        len(aic_list)
22    ):  ## si l'aic diminue pour une variable supprimee, on enleve cette variable du df X
23
24        if aic_list[i][1] < current_aic:
25            del X[aic_list[i][0]]  ## a l'interieur des crochets c'est une string
26            my_bool = 1
27
28    if my_bool == 0:
29        return "L'algorithme stepwise est terminé."
1print("Avant l'AIC, le nombre de variables explicatives est :", X_train.shape[1])
Avant l'AIC, le nombre de variables explicatives est : 24
1while True:
2    if (
3        stepwise(X_train, Y_train, regressor="linearOLS")
4        == "L'algorithme stepwise est terminé."
5    ):
6        break
1stepwise(X_train, Y_train, regressor="linearOLS")
"L'algorithme stepwise est terminé."
1print("Après l'AIC, le nombre de variables explicatives est :", X_train.shape[1])
Après l'AIC, le nombre de variables explicatives est : 14
1## on refait les modèles après sélection de variables
2
3X_test = X_test[
4    X_train.columns
5]  ## homogénéisation des données de test et d'entraînement
6
7fit = sm.OLS(Y_train, sm.add_constant(X_train)).fit()
8
9model_multiple = LinearRegression().fit(X_train, Y_train)

Distance de Cook#

1influence = fit.get_influence()
2cooks = influence.cooks_distance
1plt.scatter(X_train.index, cooks[0])
2plt.xlabel("Index des individus")
3plt.ylabel("Distances de Cook")
4plt.show()
../_images/be2e5f7bfd8770bf21f88933dd523b317e2e7bef059a3b03c8c4de4a1a7b9700.png
1cooks_indexes = [i for i, x in enumerate(cooks[0] > 0.010) if x]
2## détermine l'index dans la liste des individus dont la distance de cook dépasse 0.010
1extreme_ind_list = [X_train.iloc[[i]].index[0] for i in cooks_indexes]
2
3for ind in extreme_ind_list:
4    print(ind)
10749
6237
3968
8720
10089
1## TODO: supprimer les individus aux distances trop grandes
1influence_plot(fit)
2print("")

../_images/a03db6a14034494f76c669de358cb52eaddad8244ee4693227a2fdf67dca351c.png
1X_train.drop(extreme_ind_list + [5316], inplace=True)
2Y_train.drop(extreme_ind_list + [5316], inplace=True)
1## on refait les modèles après suppression des individus extremes
2
3X_test = X_test[
4    X_train.columns
5]  ## homogénéisation des données de test et d'entraînement
6
7fit = sm.OLS(Y_train, sm.add_constant(X_train)).fit()
8
9model_multiple = LinearRegression().fit(X_train, Y_train)

Les distances de Cook ont été largement réduites par la sélection de variables au critère AIC.

Score du modèle#

Qualité d’ajustement#

1print(f"Le R² du modèle vaut {model_multiple.score(X_train, Y_train)}")
Le R² du modèle vaut 0.62475575488369
1Y_train_hat = model_multiple.predict(X_train)
1mse_train_lineaire_multiple = mean_squared_error(Y_train, Y_train_hat)
2rmse_train_lineaire_multiple = mean_squared_error(Y_train, Y_train_hat, squared=False)
3mae_train_lineaire_multiple = mean_absolute_error(Y_train, Y_train_hat)
1print(f"MSE = {mse_train_lineaire_multiple}")
2print(f"RMSE = {rmse_train_lineaire_multiple}")
3print(f"MAE = {mae_train_lineaire_multiple}")
MSE = 3.9212570253960792
RMSE = 1.9802164087281167
MAE = 1.4484641568191754
1print(f"MSE = {mean_squared_error(Y_train, Y_train_hat)}")
2print(f"RMSE = {mean_squared_error(Y_train, Y_train_hat, squared=False)}")
3print(f"MAE = {mean_absolute_error(Y_train, Y_train_hat)}")
MSE = 3.9212570253960792
RMSE = 1.9802164087281167
MAE = 1.4484641568191754

Qualité de prédiction#

Train / test split

1print(
2    f"R² du modèle sur les données d'entraînement = {model_multiple.score(X_train, Y_train)}"
3)
4print(f"R² du modèle sur les données de test = {model_multiple.score(X_test, y_test)}")
R² du modèle sur les données d'entraînement = 0.62475575488369
R² du modèle sur les données de test = 0.6129578160565867
1y_pred = model_multiple.predict(X_test)
1plt.scatter(y_test, y_pred, color="black")
2abline(1, 0)
3plt.title("Valeurs prédites contre les vraies valeurs : données de test du modèle")
Text(0.5, 1.0, 'Valeurs prédites contre les vraies valeurs : données de test du modèle')
../_images/81c3d119a684d66a3f08b7fe9ee176eaf573de7639aca20dc570dabc6fe5f5f9.png
1affiche_score(model, y_test, y_pred)
MSE = 3.923373743247052
RMSE = 1.9807508029146554
MAE = 1.4130411421162983
1nom_modele = "Régression multiple"
2ajout_score(model, nom_modele, y_test, y_pred)

Régression par modèle linéaire généralisé (GLM) : régression de Poisson#

  • Notons $Y$ notre variable d’intérêt et $X_1, …, X_P$ nos variables explicatives.

  • On suppose que Y suit une loi de Poisson de paramètre $\lambda \in \mathbb{R+*}$

  • On cherche à déterminer les coefficients de l’équation :

$\log(\mathrm{E}(Y|X)) = \log( \lambda ) = \alpha + \beta_1 X_1 + \beta_2 X_2 + … + \beta_P X_P $

Ce qui revient à déterminer les coefficients de l’équation :

$\lambda = e^{\alpha + \beta_1 X_1 + \beta_2 X_2 + … + \beta_P X_P}$

L’objectif de la régression est d’estimer $\alpha, \beta_1, …, \beta_P$. Une fois ces coefficients estimés, on peut déterminer, pour un nouveau vecteur $X$, $E(Y|X) = e^{\alpha + \beta_1 X_1 + \beta_2 X_2 + … + \beta_P X_P}$

Hypothèses à vérifier :#


- Avant de choisir la régression de Poisson :
  1. La variable d’intérêt doit être une variable de comptage, à valeurs entières positives.

  2. La variable d’intérêt doit suivre une loi de Poisson :
    $Y | X ∼ Poisson(\lambda)$, où $\lambda \in \mathbb{R+*}$ , en particulier, la moyenne de Y doit être égale à sa variance.

  3. $log(\lambda)$ est une fonction linéaire de $X_1, X_2, … , X_P$


- A posteriori :
  1. $Cov(X_i, ε_j) = 0$, pour tout $i, j \in N \times N, i \neq j$ (exogénéité) et pour chaque variable explicative $X_p$

  2. L’erreur suit une loi normale centrée i.e. $E(ε_i) = 0$

  3. L’erreur est de variance constante (homoscédasticité) i.e $Var(ε_i) = \sigma^2$, une constante

  4. Les erreurs sont indépendantes (absence d’autocorrélation) i.e. $Cov(εi, εj) = 0$, pour tout $i, j \in N \times N, i \neq j$

  5. Absence de colinéarité entre les variables explicatives, i.e. $X^t * X$ est régulière, $det(X^t * X) \neq 0$

1X = df_reg[var_categoriques_reg + var_numeriques_reg]
2Y = df["NumStorePurchases"]

Hypothèses 1 : nature de la variable d’intérêt#

La variable NumberStorePurchases désigne le nombre d’achat effectués en magasin. C’est bien une variable de comptage dont les valeurs sont entières et positives.

Hypothèse 2 : loi de Poisson ?#

 1plt.hist(df["NumStorePurchases"], density=True)
 2
 3## creating a numpy array for x-axis
 4x = np.arange(0, 20, 1)
 5
 6## poisson distribution data for y-axis
 7y = poisson.pmf(x, mu=5)
 8
 9plt.plot(x, y)
10plt.plot()
[]
../_images/825455c05224ddb0e264873aa4a960132d1191cdfd8e6558df42ce68c3cc2a44.png
1ks_statistic, p_value = kstest(df["NumStorePurchases"], "poisson", args=(5, 0))
2print(ks_statistic, p_value)
0.22040539061928222 5.04003553934594e-88
1print(
2    "Variance de Y :",
3    df["NumStorePurchases"].var(),
4    ": Espérance de Y:",
5    df["NumStorePurchases"].mean(),
6)
Variance de Y : 10.511054846064726 : Espérance de Y: 5.772904483430799
1print(
2    "La variance et l'espérance de Y ne sont pas égales. Cela peut dégrader la qualité de notre régression."
3)
La variance et l'espérance de Y ne sont pas égales. Cela peut dégrader la qualité de notre régression.

Hypothèse 3 : lien linéaire entre $log(Y)$ et $X_1, …, X_P$#

1null_index = Y[Y == 0].index
2## on élimine les 0 pour appliquer le log
 1sns.pairplot(
 2    pd.concat(
 3        [X.drop(index=null_index, axis=0), np.log(Y.drop(index=null_index, axis=0))],
 4        axis=1,
 5    ),
 6    x_vars=var_numeriques_reg[0:4],
 7    y_vars="NumStorePurchases",
 8)
 9sns.pairplot(
10    pd.concat(
11        [X.drop(index=null_index, axis=0), np.log(Y.drop(index=null_index, axis=0))],
12        axis=1,
13    ),
14    x_vars=var_numeriques_reg[4:8],
15    y_vars="NumStorePurchases",
16)
17sns.pairplot(
18    pd.concat(
19        [X.drop(index=null_index, axis=0), np.log(Y.drop(index=null_index, axis=0))],
20        axis=1,
21    ),
22    x_vars=var_numeriques_reg[8:12],
23    y_vars="NumStorePurchases",
24)
25sns.pairplot(
26    pd.concat(
27        [X.drop(index=null_index, axis=0), np.log(Y.drop(index=null_index, axis=0))],
28        axis=1,
29    ),
30    x_vars=var_numeriques_reg[12:],
31    y_vars="NumStorePurchases",
32)
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[100], line 3
      1 sns.pairplot(
      2     pd.concat(
----> 3         [X.drop(index=null_index, axis=0), np.log(Y.drop(index=null_index, axis=0))],
      4         axis=1,
      5     ),
      6     x_vars=var_numeriques_reg[0:4],
      7     y_vars="NumStorePurchases",
      8 )
      9 sns.pairplot(
     10     pd.concat(
     11         [X.drop(index=null_index, axis=0), np.log(Y.drop(index=null_index, axis=0))],
   (...)
     15     y_vars="NumStorePurchases",
     16 )
     17 sns.pairplot(
     18     pd.concat(
     19         [X.drop(index=null_index, axis=0), np.log(Y.drop(index=null_index, axis=0))],
   (...)
     23     y_vars="NumStorePurchases",
     24 )

File ~/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/pandas/util/_decorators.py:331, in deprecate_nonkeyword_arguments.<locals>.decorate.<locals>.wrapper(*args, **kwargs)
    325 if len(args) > num_allow_args:
    326     warnings.warn(
    327         msg.format(arguments=_format_argument_list(allow_args)),
    328         FutureWarning,
    329         stacklevel=find_stack_level(),
    330     )
--> 331 return func(*args, **kwargs)

File ~/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/pandas/core/frame.py:5399, in DataFrame.drop(self, labels, axis, index, columns, level, inplace, errors)
   5251 @deprecate_nonkeyword_arguments(version=None, allowed_args=["self", "labels"])
   5252 def drop(  # type: ignore[override]
   5253     self,
   (...)
   5260     errors: IgnoreRaise = "raise",
   5261 ) -> DataFrame | None:
   5262     """
   5263     Drop specified labels from rows or columns.
   5264 
   (...)
   5397             weight  1.0     0.8
   5398     """
-> 5399     return super().drop(
   5400         labels=labels,
   5401         axis=axis,
   5402         index=index,
   5403         columns=columns,
   5404         level=level,
   5405         inplace=inplace,
   5406         errors=errors,
   5407     )

File ~/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/pandas/util/_decorators.py:331, in deprecate_nonkeyword_arguments.<locals>.decorate.<locals>.wrapper(*args, **kwargs)
    325 if len(args) > num_allow_args:
    326     warnings.warn(
    327         msg.format(arguments=_format_argument_list(allow_args)),
    328         FutureWarning,
    329         stacklevel=find_stack_level(),
    330     )
--> 331 return func(*args, **kwargs)

File ~/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/pandas/core/generic.py:4505, in NDFrame.drop(self, labels, axis, index, columns, level, inplace, errors)
   4503 for axis, labels in axes.items():
   4504     if labels is not None:
-> 4505         obj = obj._drop_axis(labels, axis, level=level, errors=errors)
   4507 if inplace:
   4508     self._update_inplace(obj)

File ~/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/pandas/core/generic.py:4546, in NDFrame._drop_axis(self, labels, axis, level, errors, only_slice)
   4544         new_axis = axis.drop(labels, level=level, errors=errors)
   4545     else:
-> 4546         new_axis = axis.drop(labels, errors=errors)
   4547     indexer = axis.get_indexer(new_axis)
   4549 # Case for non-unique axis
   4550 else:

File ~/.cache/pypoetry/virtualenvs/customer-base-analysis-F-W2gxNr-py3.10/lib/python3.10/site-packages/pandas/core/indexes/base.py:6934, in Index.drop(self, labels, errors)
   6932 if mask.any():
   6933     if errors != "ignore":
-> 6934         raise KeyError(f"{list(labels[mask])} not found in axis")
   6935     indexer = indexer[~mask]
   6936 return self.delete(indexer)

KeyError: '[8475, 5555, 4931, 11181] not found in axis'
 1for x in X[var_numeriques_reg].columns:
 2    corr = pearsonr(
 3        X.drop(index=null_index, axis=0)[x], np.log(Y.drop(index=null_index, axis=0))
 4    )
 5    print(
 6        "Le coefficient de corrélation linéaire entre log(Y) et",
 7        x,
 8        "vaut",
 9        corr[0],
10        "la pvalue vaut",
11        corr[1],
12    )

Il existe bien un lien linéaire entre $log(Y)$ et les variables explicatives.

Réalisation du modèle#

1X_train, X_test, Y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=0)
1model_poisson = PoissonRegressor().fit(X_train, Y_train)
2Y_train_hat = model_poisson.predict(X_train)

Ici, notre fonction de lien est $f(\lambda) = log(\lambda)$
Elle est utilisée par défaut par sci-kit learn

1plt.scatter(Y_train, Y_train_hat, color="black")
2plt.title("Valeurs prédites contres vraies valeurs (donnéees d'entraînement)")
3abline(1, 0)

Hypothèse 4 : exogénéité sur les variables numériques#

(Cela n’a pas de sens de tester l’exogénéité sur les variables catégoriques car la notion de corrélation ne fonctionne pas avec celles-ci.)

1residuals = Y_train - Y_train_hat
 1for x in X_train[var_numeriques_reg].columns:
 2    corr = pearsonr(X_train[x], residuals)
 3    print(
 4        "Le coefficient de corrélation entre",
 5        x,
 6        "et les residus vaut",
 7        corr[0],
 8        "et la pvalue associée vaut",
 9        corr[1],
10    )

Il n’y a pas d’endogénéité.

Hypothèse 5 : l’erreur suit une loi normale centrée i.e. $E(ε_i) = 0$#

1plt.hist(residuals, density=True)
2
3x = np.linspace(-6, 6, 100)
4plt.plot(x, stats.norm.pdf(x, 0, 1))
5plt.title("Histogramme des résidus en superposition avec la densité de la loi normale")
6plt.show()
1print("La moyenne des résidus vaut", statistics.mean(residuals))
1sm.qqplot(residuals, line="45")
2print("On constate sur le qqplot que les points ne suivent pas la droite x = y")
1print(
2    "Un test de shapiro, pour tester l'hypothèse de normalité, nous donne une pvalue de",
3    stats.shapiro(residuals)[1],
4    ". On rejette l'hypothèse nulle et on conclut que les résidus ne suivent pas une loi normale.",
5)

Hypothèse 6 : homoscédasticité#

1plt.plot(residuals, "bo")
2plt.title("Nuage de points des résidus")
3abline(0, 6.5)
4abline(0, -6.5)
1fit = sm.GLM(Y_train, sm.add_constant(X_train), family=sm.families.Poisson()).fit()
1fit.summary()
1res_names = ["F statistic", "p-value"]
2gq_test = sms.het_goldfeldquandt(fit.resid_response, fit.model.exog)
3lzip(res_names, gq_test)

La pvalue du test de Goldfeld-Quandt est supérieure à 0.05 dont on conserve l’hypothèse d’homoscédasticité au seuil de 5% (et même au delà).

Hypothèse 7 : absence d’autocorrélation#

1sm.graphics.tsa.plot_acf(residuals)
2print("On remarque une absence d'autocorrélation.")

Hypothèse 8 : absence de multicolinéarité#

1vif = [
2    variance_inflation_factor(sm.add_constant(X_train).values, i)
3    for i in range(sm.add_constant(X_train).shape[1])
4]
5## Rq: la fonction VIF attend une constante dans le dataframe d'entree
1print(pd.DataFrame({"VIF": vif[1:]}, index=X_train.columns))

Réduction du nombre de variables explicatives par le critère AIC#

1print("Avant l'AIC, le nombre de variables explicatives est :", X_train.shape[1])
1while True:
2    if (
3        stepwise(X_train, Y_train, regressor="GLMpoisson")
4        == "L'algorithme stepwise est terminé."
5    ):
6        break
1stepwise(X_train, Y_train, regressor="GLMpoisson")
1print("Après l'AIC, le nombre de variables explicatives est :", X_train.shape[1])
1## on refait les modèles après sélection de variables
2
3X_test = X_test[
4    X_train.columns
5]  ## homogénéisation des données de test et d'entraînement
6
7fit = sm.GLM(Y_train, sm.add_constant(X_train), family=sm.families.Poisson()).fit()
8
9model_poisson = PoissonRegressor().fit(X_train, Y_train)

Distance de Cook#

1influence = fit.get_influence()
2cooks = influence.cooks_distance
1plt.scatter(X_train.index, cooks[0])
2plt.xlabel("Index des individus")
3plt.ylabel("Distances de Cook")
4plt.show()
1cooks_indexes = [i for i, x in enumerate(cooks[0] > 0.010) if x]
2## détermine l'index dans la liste des individus dont la distance de cook dépasse 0.010
1extreme_ind_list = [X_train.iloc[[i]].index[0] for i in cooks_indexes]
2
3for ind in extreme_ind_list:
4    print(ind)
1X_train.drop(extreme_ind_list, inplace=True)
2Y_train.drop(extreme_ind_list, inplace=True)
1## on refait les modèles après suppression des individus extremes
2
3X_test = X_test[
4    X_train.columns
5]  ## homogénéisation des données de test et d'entraînement
6
7fit = sm.GLM(Y_train, sm.add_constant(X_train), family=sm.families.Poisson()).fit()
8
9model_poisson = PoissonRegressor().fit(X_train, Y_train)
1## Remarque : statsmodels ne fait pas d'influence plot pour les modeles GLM

Score du modèle#

Qualité d’ajustement#

1print(f"Le R² du modèle vaut {model_poisson.score(X_train, Y_train)}")
1Y_train_hat = model_poisson.predict(X_train)
1mse_train_poisson = mean_squared_error(Y_train, Y_train_hat)
2rmse_train_poisson = mean_squared_error(Y_train, Y_train_hat, squared=False)
3mae_train_poisson = mean_absolute_error(Y_train, Y_train_hat)
1print(f"MSE = {mse_train_poisson}")
2print(f"RMSE = {rmse_train_poisson}")
3print(f"MAE = {mae_train_poisson}")

Qualité de prédiction#

Train / test split

1print(
2    f"R² du modèle sur les données d'entraînement = {model_poisson.score(X_train, Y_train)}"
3)
4print(f"R² du modèle sur les données de test = {model_poisson.score(X_test, y_test)}")
1y_pred = model_poisson.predict(X_test)
1plt.scatter(y_test, y_pred, color="black")
2abline(1, 0)
3plt.title("Valeurs prédites contre les vraies valeurs : données de test du modèle")
1affiche_score(model, y_test, y_pred)
1nom_modele = "GLM Poisson"
2ajout_score(model, nom_modele, y_test, y_pred)
1print("Constante : ", model_poisson.intercept_)
2for i in range(len(model_poisson.coef_)):
3    print(f" beta_{i + 1} = {model_poisson.coef_[i]} ")

Régression polynomiale#

Création des données#

On conserve les mêmes variables explicatives et la même variable d’intérêt, mais en séparant les variables catégoriques des variables numériques qu’on transformera en monômes.
Ceci car cela n’aurait pas de sens d’élever des variables catégoriques au carré et afin d’éviter l’explosion combinatoire.

1X_ref = df_reg[var_numeriques_reg + var_categoriques_reg]
1X_numeriques = df_reg[var_numeriques_reg]
2X_categoriques = df_reg[var_categoriques_reg]
3Y = df_reg["NumStorePurchases"]
1## Transformer les données en matrice de caractéristiques polynomiales
2poly = PolynomialFeatures(degree=2)
3
4X_poly = poly.fit_transform(X_numeriques)
5## Remarque : pendant la transformation, l'ordre des lignes est conservé, ce qui permettra de remettre les index
 1## trouvé sur stack over flow :
 2## permet de créer un df à partir de l'objet numpy array en sortie de la fonction fit_transform de sklearn
 3
 4target_feature_names = [
 5    "x".join(["{}^{}".format(pair[0], pair[1]) for pair in tuple if pair[1] != 0])
 6    for tuple in [zip(X.columns, p) for p in poly.powers_]
 7]
 8## détermine les noms de colonnes
 9
10
11X_poly = pd.DataFrame(X_poly, columns=target_feature_names)
1## je remets les indexes au df X_poly afin de pouvoir le concaténer avec le df des var categoriques avant la régression
2X_poly = X_poly.set_index(pd.Index(X_categoriques.index))
1X = pd.concat([X_poly, X_categoriques], axis=1)
1X.shape

Bien évidemment, il y a de la redondance dans les variables explicatives et donc de la colinéarité structurelle.

Réalisation du modèle#

1X_train, X_test, Y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=0)
1model_poly = LinearRegression().fit(X_train, Y_train)
2Y_train_hat = model_poly.predict(X_train)
1plt.scatter(Y_train, Y_train_hat, color="black")
2plt.title("Valeurs prédites contres vraies valeurs (donnéees d'entraînement)")
3abline(1, 0)

Réduction du nombre de variables explicatives par le critère AIC#

1print("Avant l'AIC, le nombre de variables explicatives est :", X_train.shape[1])
1while True:
2    if (
3        stepwise(
4            X_train, list(Y_train), regressor="linearOLS"
5        )  ## il faut transformer Y en liste sinon cela bug, je ne sais pas pourquoi
6        == "L'algorithme stepwise est terminé."
7    ):
8        break
1stepwise(X_train, list(Y_train), regressor="linearOLS")
1print("Après l'AIC, le nombre de variables explicatives est :", X_train.shape[1])

La procédure AIC a permi de purifier notre modèle de 77 variables explicatives.

 1## on refait les modèles après sélection de variables
 2
 3X_test = X_test[
 4    X_train.columns
 5]  ## homogénéisation des données de test et d'entraînement
 6
 7fit = sm.OLS(
 8    list(Y_train), sm.add_constant(X_train)
 9).fit()  ## comme il existe deja une constante dans X_train, statsmodels n'en rajoute pas une deuxieme
10
11model_poly = LinearRegression().fit(X_train, Y_train)

Distance de Cook#

1influence = fit.get_influence()
2cooks = influence.cooks_distance
1plt.scatter(X_train.index, cooks[0])
2plt.xlabel("Index des individus")
3plt.ylabel("Distances de Cook")
4plt.show()
1cooks_indexes = [i for i, x in enumerate(cooks[0] > 0.02) if x]
2## détermine l'index dans la liste des individus dont la distance de cook dépasse 0.010
1extreme_ind_list = [X_train.iloc[[i]].index[0] for i in cooks_indexes]
2
3for ind in extreme_ind_list:
4    print(ind)
1influence_plot(fit)
2print("")
1X_train.drop(extreme_ind_list + [6237, 9058], inplace=True)
2Y_train.drop(extreme_ind_list + [6237, 9058], inplace=True)
 1## on refait les modèles après suppression des individus extremes
 2
 3X_test = X_test[
 4    X_train.columns
 5]  ## homogénéisation des données de test et d'entraînement
 6
 7fit = sm.OLS(
 8    list(Y_train), sm.add_constant(X_train)
 9).fit()  ## comme il existe deja une constante dans X_train, statsmodels n'en rajoute pas une deuxieme
10
11model_poly = LinearRegression().fit(X_train, Y_train)
1influence = fit.get_influence()
2cooks = influence.cooks_distance
1plt.scatter(X_train.index, cooks[0])
2plt.xlabel("Index des individus")
3plt.ylabel("Distances de Cook")
4plt.show()
1influence_plot(fit)
2print("")

Score du modèle#

Qualité d’ajustement#

1print(f"Le R² du modèle vaut {model_poly.score(X_train, Y_train)}")
1Y_train_hat = model_poly.predict(X_train)
1mse_train_poly = mean_squared_error(Y_train, Y_train_hat)
2rmse_train_poly = mean_squared_error(Y_train, Y_train_hat, squared=False)
3mae_train_poly = mean_absolute_error(Y_train, Y_train_hat)
1print(f"MSE = {mse_train_poly}")
2print(f"RMSE = {rmse_train_poly}")
3print(f"MAE = {mae_train_poly}")

Qualité de prédiction#

Train / test split

1print(
2    f"R² du modèle sur les données d'entraînement = {model_poly.score(X_train, Y_train)}"
3)
4print(f"R² du modèle sur les données de test = {model_poly.score(X_test, y_test)}")
1y_pred = model_poly.predict(X_test)
1plt.scatter(y_test, y_pred, color="black")
2abline(1, 0)
3plt.title("Valeurs prédites contre les vraies valeurs : données de test du modèle")
1print(f"MSE = {mean_squared_error(y_test, y_pred)}")
2print(f"RMSE = {mean_squared_error(y_test, y_pred, squared=False)}")
3print(f"MAE = {mean_absolute_error(y_test, y_pred)}")

La mesure de la qualité d’ajustement à nos données de test est biaisée par des valeurs prédites extrêmes.

1sns.histplot(y_pred)
1y_pred_new = y_pred[y_pred > 0]
2y_test_new = y_test[y_pred > 0]
1plt.scatter(y_test_new, y_pred_new, color="black")
2abline(1, 0)
3plt.title("Valeurs prédites contre les vraies valeurs : données de test du modèle")
1affiche_score(model, y_test, y_pred)
1nom_modele = "Régression polynomiale"
2ajout_score(model, nom_modele, y_test, y_pred)

Corrigé de ces erreurs, le modèle polynomial est notre meilleur modèle.

1print("Constante : ", model_poly.intercept_)
2for i in range(len(model_poly.coef_)):
3    print(f" beta_{i + 1} = {model_poly.coef_[i]} ")

Régression PLS#

Comme nous avons de la multi-colinéarité dans notre modèle, nous allons essayer une méthode de régression robuste face à ce phénomène : la régression PLS.

1X = pd.get_dummies(df_transforme[var_numeriques].drop(columns=["NumStorePurchases"]))
1y = df[["NumStorePurchases"]].astype(int)
1X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
1liste_mae_pls = []
2for n in range(1, 14):
3    pls = PLSRegression(n_components=n)
4
5    pls.fit(X_train, y_train)
6    pls.score(X_train, y_train)
7    y_pred = pls.predict(X_test)
8
9    liste_mae_pls.append(mean_absolute_error(y_test, y_pred))
1plt.title("MAE en fonction du nombre de composantes PLS")
2plt.plot(range(1, 14), liste_mae_pls)
3plt.scatter(np.argmin(liste_mae_pls) + 1, np.min(liste_mae_pls), label="minimum")
4plt.legend()
1print(np.argmin(liste_mae_pls) + 1)
1pls = PLSRegression(n_components=8)
1pls.fit(X_train, y_train)
1pls.score(X_train, y_train)
1y_pred = pls.predict(X_test)
1affiche_score(model, y_test, y_pred)
1nom_modele = "Régression PLS"
2ajout_score(model, nom_modele, y_test, y_pred)

XGBoost#

1X = pd.get_dummies(df_transforme.drop(columns=["NumStorePurchases", "Dt_Customer"]))
1y = df[["NumStorePurchases"]].astype(int)
1X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
1tuned_xgb = xgboost.XGBRegressor(
2    n_estimators=1000,
3    learning_rate=0.05,
4    n_jobs=4,
5    eval_metric="mae",
6    early_stopping_rounds=20,
7    random_state=0,
8)
1tuned_xgb.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False)
1y_pred = tuned_xgb.predict(X_test)
1affiche_score(model, y_test, y_pred)
1nom_modele = "XGBoost"
2ajout_score(model, nom_modele, y_test, y_pred)

Réseau de neurones#

1X_train = np.asarray(X_train).astype("float32")
2y_train = np.asarray(y_train).astype("float32")
3X_test = np.asarray(X_test).astype("float32")
4y_test = np.asarray(y_test).astype("float32")
1X_train.shape
1model = keras.Sequential(
2    [
3        layers.Dense(4, activation="relu", input_shape=[35]),
4        layers.Dense(4, activation="relu"),
5        layers.Dense(1, activation="sigmoid"),
6    ]
7)
1model.compile(
2    optimizer="adam",
3    loss="mean_absolute_error",
4)
1early_stopping = keras.callbacks.EarlyStopping(
2    patience=10,
3    min_delta=0.001,
4    restore_best_weights=True,
5)
 1history = model.fit(
 2    X_train,
 3    y_train,
 4    ## validation_data=(X_test, y_test),
 5    validation_split=0.2,
 6    batch_size=512,
 7    epochs=1000,
 8    callbacks=[early_stopping],
 9    verbose=0,  ## hide the output because we have so many epochs
10)
1history_df = pd.DataFrame(history.history)
2## Start the plot at epoch 5
3history_df.loc[5:, ["loss", "val_loss"]].plot()
1y_pred = model.predict(X_test)
1affiche_score(model, y_test, y_pred)
1nom_modele = "Réseau de Neurones"
2ajout_score(model, nom_modele, y_test, y_pred)

Sauvegarde des données#

1score_modeles_df = pd.DataFrame(score_modeles, columns=["Modèle", "Métrique", "Valeur"])
1score_modeles_df.to_csv(f"{data_folder}/results/regressions.csv", index=False)