Régressions linéaires en python

Régressions simples et multiples

L'essentielle de cette page

Python se prête aux régressions linéaires simples ou multiples. Cela nécessite l'utiliation de module particulers :

  • numpy et pandas (pour avoir des nombres au bon format)

  • pyplot de matplotlib (pour visualiser les données sous forme de graphiques)

  • sklear (sci-kit learn) et statsmodels.api pour réaliser les régressions et les contrôler

  • Remarque : des sources externes à la fin et la possibilité de réaliser la même chose plus simplement avec R (cf. Aide à l'utilisation de R)

Lorsqu'on souhaite prédire des catégories (catégories 0 et 1 par exemple), on doit songer à faire appel à une régression logistique ou à un classifieur bayésien naïf.

Étape 0 - Partons d'un exemple ! Imaginons un échantillon d'une population, dont on a la taille, le poids et l'IMC (Indice de Masse Corporelle) !

Essayons de voir si on peut retrouver la relation entre taille, poids et IMC sachant qu'on connait pour une fois la réponse IMC = poids/(taille/100)²

Simulons ce jeu de données :

import numpy as np # Importe les fonctions de numpy

from numpy.random import normal as norm # Import juste une fonction de numpy pour simplifier son nom par norm

from numpy import sqrt as sqrt # Import juste une fonction de numpy pour simplifier son nom par sqrt

nb = 180 # Nombre d'individus

poids = np.round(norm(size = nb,loc = 65,scale = 11),0) # Simulation de poids moyens autour de 65 (SD : 11)

imc = norm(size = nb,loc = 22,scale = 5) # Simuations des IMC de ces individus

taille = np.round(sqrt(poids/imc)*100,0) # Déduction d'une taille à partir de ces IMC

taille_bis = np.round(norm(size = nb,loc = 170,scale = 11),0) # Simulation de tailles pour ces individus

taille_biss = np.round(norm(size = nb,loc = 170,scale = 11),0) # Simulation d'autres tailles pour ces individus

taille = np.round((taille+taille_bis+taille_biss)/3,0) # Moyenne des 3 jeux de tailles (2 simulés et 1 déduits d'IMC)

imc = np.round(poids/(taille/100)**2,1) # Calculs des IMC à partir de taille et poids

sexe = ["F"]*90+["M"]*90

num = list(range(1,181,1)) # numérotation de 1 à 180

import pandas as df # Importation de pandas pour la gestion des tableaux de données (dataframes)

from pandas import DataFrame as DataFrame # Importation uniquement de la fonction DataFrame pour réduire son écriture

compil = {'num':num,'poids':poids,'taille':taille,'imc':imc,'sexe':sexe}

compil = DataFrame(compil) # Compilation des valeurs en dataframe (il faut que ce soit en dictionnaire ci-avant)

Etape 1 - Décrire le jeu de données afin de comprendre les relations entre Y (l'IMC) et les variables X1 et X2 (poids et taille)

1.0- Visualiser un graphique globale de relations entre l'ensembles des variable d'un tableau (dataframe)

  • Croiser automatiquement toutes les variables (matrices de scatter plot) avec la fonction scatter_matrix() de pandas

from matplotlib import pyplot as plt

from pandas.plotting import scatter_matrix


from pandas.plotting import scatter_matrix

scatter_matrix(compil, alpha = 0.2, figsize = (6, 6), diagonal = 'kde',color="red")

plt.show() # On voit que les variables discrètes sont éliminées

A tester : remplacer diagonal = 'kde' par 'hist'

Matrice de scatter plot en python (élimination des variables discrètes)
  • Si je veux éliminer des colonnes, je peux définir l'index des colonnes retenus et faire appel à iloc

from pandas.plotting import scatter_matrix

scatter_matrix(compil.iloc[:,1:4], alpha = 0.2, figsize = (6, 6), diagonal = 'kde')

plt.show()

Ici, on trace la matrice de scatter-plot pour les colonnes de notre choix
  • Si je veux éliminer des colonnes en définissant les colonnes négatives, celles à éliminer et non celles à garder, je ne peux utiliser des index négatifs comme sous R

# Si je voulais ne pas afficher les colonnes 0 et 4...

# Je ne peux mettre des indices négatifs (contrairement à R)

# En revanche, je peux utiliser la fonction drop() pour appeler les fonctions que je ne veux pas par leur nom

mon_index=compil.columns.drop(["num","imc","sexe"])

from pandas.plotting import scatter_matrix

scatter_matrix(compil[mon_index], alpha = 0.6, figsize = (6, 6), diagonal = 'kde',color="orange")

plt.show()


# Je peux aussi automatiser l'extraction des colonnes que je veux pas en raisonnant avec les indices

mes_col = set(range(5)) # J'ai 5 colonnes

mes_col_neg = set([0,4]) # Je ne veux plus les colonnes 0 et 4

mon_index = list(mes_col-mes_col_neg)

# Tracer la matrice

from pandas.plotting import scatter_matrix

scatter_matrix(compil.iloc[:,mon_index], alpha = 0.2, figsize = (6, 6), diagonal = 'kde')

plt.show()

Ce graphique a été construit en définissant les variables que l'on ne désire pas.

1.2- Établir un lissage de la relation poids // IMC.

  • Visualiser la relation entre poids et IMC (exemple)

import matplotlib.pyplot as plt

plt.scatter(compil.poids,compil.imc, c="#D4BA2D") ; plt.xlabel('Poids (kg)') ; plt.ylabel('IMC') ; plt.show()

import numpy as np


x = compil.poids

y = compil.imc


from matplotlib.pyplot import plot

from matplotlib.pyplot import show

plot(x, y,'o')

x_s, y_s = smooth(x,y)

plot(x_s,y_s, 'r-', lw=2)

x_s, y_s = smooth(x,y,box_percent=0.25)

plot(x_s,y_s, 'g-', lw=2)

show()

cf. aussi lowess

Courbe de lissage python - smooth - lowess - moyenne glissante - moyenne mobile

Fonction smooth - Code à copier-coller - Cliquer sur ce titre pour dérouler le code.

def smooth(x,y, box_percent=0.05,res=50,median=True):

surface = max(x)-min(x)

my_pas = np.arange(min(x),max(x),surface/res)

box = surface*box_percent

demi_box = box/2

y_sortie = np.array([])

x_sortie = np.array([])

for myx in my_pas :

temp = [y[i] for i in range(len(x)) if ((x[i]>=(myx-demi_box))and(x[i]<=(myx+demi_box)))]

if median==True :

temp_y = np.median(temp)

else :

temp_y = np.mean(temp)

#print(temp_y)

y_sortie = np.append(y_sortie,temp_y)

#print(y_sortie)

x_sortie = np.append(x_sortie,myx)

return x_sortie, y_sortie

En projet : développer une accélération de la fonction et les intervalles de confiance en polygones grisés.

Développement en cours : suivre ces liens externes :

1.3- Décrire les relations statistiques entre les variables X1, X2... et Y avec les fonctions cor(), cor.test() et le R²

  • Je peux faire une matrice de corrélation

mycor = compil.corr() ; print(mycor)

  • Pour faire un test de corrélation précis avec p-value (l'équivalent de cor.test() de R), il faut faire appel au modul scipy

import scipy

my_cor, pval = scipy.stats.pearsonr(poids,imc)

# même si ma corrélation est faible (valeur <<1), la valeur 2 montre une p-value très significative

print(my_cor)

print(pval)

Dans une relation simple où y dépend d'un seul x (y = f(x)), la corrélation correspond à la racine carrée du R².

Étape 2 - Réaliser le modèle de régression linéaire

A taper dans la console windows pour installer des librairies utiles (Keras est optionnel, à développer sur ce site)

python -m pip install sklearn Keras

python -m pip install --upgrade pip

pip3 install sklearn

Etape 2.1 - Définir les valeurs X et Y (attention, tous les formats ne sont pas acceptés)

Option 1 - Les valeurs X sont des arrays de type numpy. X doit être verticalisé avec la fonction reshape().

X = (np.array(compil.poids)).reshape(-1,1)

Y = compil.imc

Option 2- Même si X est une colonne extraite d'une dataframe, il faut forcer sa reconnaissance dans ce format

X = DataFrame(compil.poids)

Y = compil.imc

Etape 2.2 - Mettre en place le modèle de régression

from sklearn.linear_model import LinearRegression

myreg = LinearRegression()

myreg = myreg.fit(X,Y)

Autre méthode se rapprochant de l'écriture de R et offrant un bilan plus complet

import statsmodels.formula.api as sm

model = sm.ols(formula='imc ~ poids',data=compil)

# model = sm.ols(formula='imc ~ poids +0',data=compil) # Même régression passant par 0

myreg = model.fit()

print(myreg.summary())

Etape 2.3 - Visualiser la régression linéaire

plt.scatter(X,Y,c="orange") ; plt.plot(X,myreg.predict(X),"-r",color="#D42D85", linewidth=10) ; plt.xlabel("Poids") ; plt.ylabel("IMC") ; plt.show()

Etape 2.4 - Récupérer l'équation de la droite

print(myreg.intercept_) # B0 (b)

print(myreg.coef_) # B1 (coefficient directeur)

print(myreg._residues) # A étudier pour avoir tous les résidus. sinon on peut faire la différence entre Y et myreg.predict(X)

print('coef dir =',myreg.coef_, 'ord orgine =',myreg.intercept_)

Étape 3 - Contrôler le modèle de régression linéaire

La première chose à vérifier est que le modèle à une bonne p-value (significatif) et que ses coefficients ont aussi des bonnes p-values.

Il faut aussi contrôler les p-values des coefficients et du modèle.

  • La p-value du modèle se calcule en faisant une simple corrélation entre y et fitted.values.

  • Les p-values des coefficients sont à extraire de diverses façon selon statsmodels et sklearn

statsmodels :

myreg.pvalues

# ou encore :

for attributeIndex in range (0, 2):

print(myreg.pvalues[attributeIndex])

sklearn

A voir...

3.0- On va vérifier d'abord que le modèle de régression semble adapté

Les résidus doivent avoir à peu près la même valeur moyenne pour les différentes valeurs de y prédites (fitted values).

On peut vérifier que cette condition est respectée par une 1) représentation graphique ou 2) par un test statistique (test de Rainbow).

1) Représentation graphique

Sur ce graphique (résidues = f(fitted.values), la ligne rouge doit être approximativement horizontale :

A partir d'une régression sklearn

# Les résidues

residues = Y-myreg.predict(X) # régression sklearn

residues = myreg.resid_pearson # régression statmodels

# Les valeurs projetées sur la régression (y théorique)

fitted_values = myreg.predict(X)

# Représentation graphique

from matplotlib.pyplot import plot, scatter, show, xlabel, ylabel

scatter(fitted_values,residues)

xlabel("Fitted values")

ylabel("Résidus")

# en reprenant la fonction smooth() plus haut

xs , ys = smooth(fitted_values,residues,box_percent=0.25,res=30)

plot(xs,ys,"-r")

show()

Remarque : le code de la fonction smooth() est à copier-coller (disponible ci-dessus).

2) Test statistique de Rainbow

Le test de Rainbow vérifie l'hypothèse nulle suivante : la représentation statistique est bien linéaire. On parle ici de contrôle de l'adéquation. La p-value renvoyée par ce test devrait donc être supérieure à 0,05 pour qu'on considère que le modèle de régression peut être conservé.

from statsmodels.stats.diagnostic import linear_rainbow

Ftest, pval = linear_rainbow(myreg)

print(pval)

3.1- Contrôler l'indépendance des résidus

Il faut ensuite contrôler l'indépendance des résidus. On peut le faire là-aussi via 1) une représentation graphique ACP (auto-correlation function) ou 2) par le test statistique de Durbin-Watson.

1) Graphique d'autocorrélation des résidus (ACF)

Les bâtonnets 1 à n ne doivent pas dépasser la zone bleue.

from statsmodels.graphics.tsaplots import plot_acf

plot_acf(residues)

source code

2) Test de Durbin-Watson

Le test de Durbin-Watson (un exemple interne pour en savoir plus) permet de détecter ces autocorrélations par un test objectif. La valeur renvoyée doit s'approcher de 2. Plus elle s'en éloigne, plus on risque d'avoir des autocorrélations.

Seuils admis (et très subjectifs) : pas d'autocorrélation entre 1.6 et 2.4... (quoi que...)

from statsmodels.stats.stattools import durbin_watson

# Durbin-Watson test

print(durbin_watson(myreg.resid))

3.2- Vérifier que les résidus suivent une distribution normale

Les résidus standards doivent être répartis selon la loi normale. On peut vérifier cela 1) par des graphiques (histogramme ou QQ-plot) ou 2) par des tests statistiques (test Shapiro-Wilk ou test de Jarque-Berra).

Récupérons les résidus :

  • A partir d'une régression statsmodels

residues = myreg.resid_pearson

  • A partir d'une régression sklearn

residues = myreg.resid # Y-myreg.predict(X)

Remarque : il faut ici utiliser les résidus standards : chaque résidu doit être divisé par la racine carrée de la somme des carrés des résidus divisée par N.

residues_std = residues/sqrt(sum(residues**2)/(len(residues)-1))

1) Contrôle graphique par histogramme ou QQ-plot

A) Contrôle de la répartition normale des résidus avec un histogramme :

Histogramme :

plt.hist(residues_std)

plt.ylabel('Count')

plt.xlabel('Normalized residuals')

show()

B) Plus pertinent encore est de faire appel au diagramme Quantile-Quantile ou QQ plot.

import statsmodels.api as sm

import scipy.stats as stats

#

fig = sm.qqplot(residues_std, stats.t, fit=True,line='45')

plt.title("QQ plot")

plt.show()

source code

2) Contrôle par un test statistique

Le test de Shapiro (qui fonctionne pour les petits échantillons inférieurs à 50) permet de contrôler l'hypothèse nulle d'une répartition normale. Si p-value > 0.05 <-> Normalité.

from scipy.stats import shapiro

x, pval = shapiro(residues_std) ; print(pval)

Pour les grands échantillons, on peut aussi faire de test de Jarque-Berra :

from scipy.stats import jarque_bera

x, pval = jarque_bera(residues_std) ; print(pval)

3.3- Vérifier que la variance des résidus est constante (distribution homogène)

Un autre contrôle permet de vérifier que la variance des résidus est constante (cas d'homoscédasticité).

Là encore, 1) une représentation graphique suffit ou 2) le test statistique de Breusch-Pagan.

On va cette fois-ci raisonner sur les résidus standardisés, la racine carrée des résidus standards.

sqrt_residues_std = sqrt(abs(residues_std)) # ne fonctionne pas si on ne met pas abs


1) Contrôle graphique de la variance des résidus standardisés

Pour cela on trace le graphique suivant qui met en relation les racines carrées des résidus (résidus standardisés) en fonction des valeurs théoriques (fitted-values) de Y prédites par l'équation de la régression.

fitted_values = myreg.predict(X)

from matplotlib.pyplot import plot, scatter, show, xlabel, ylabel

scatter(fitted_values,sqrt_residues_std)

xlabel("Fitted values")

ylabel("Racine carrée des résidus standardisés")

# en reprenant la fonction smooth() plus haut

xs , ys = smooth(fitted_values,sqrt_residues_std,box_percent=0.25,res=30)

plot(xs,ys,"-r")

show()

2) Test de Breusch-Pagan

Cet test renvoie une p-value qui interroge la variance des résidus. Si la p-value est inférieure à 0,05, on doit rejeter la régression comme présentant de l'hétéroscédasticité.

from statsmodels.stats.api import het_breuschpagan

lagrande, pval, f, fpval = het_breuschpagan(myreg.resid, myreg.model.exog)

On notera qu'on ne donne pas à avaler les résidus standardisés à cette fonction. Elle se charge de mener les bons calculs.

myreg.model.exog correspond à une array qui regroupe les indices des résidus les plus susceptibles d'induire une hétéroscédasticité.

3.4- Évaluer les points qui ont une grande influence sur la régression afin de les écarter s'il s'agit de points potentiellement aberrants.

from statsmodels.graphics.regressionplots import *

influence_plot(myreg) # myreg doit être un model de statsmodels

plt.xlim(0,0.06) # paramétrage manuel

plt.show()

Leverage : Effet de levier : Dans les statistiques et en particulier dans l'analyse de régression, l'effet de levier est une mesure de la distance qui sépare les valeurs des variables indépendantes d'une observation de celles des autres observations.

Dans ce graphique, x exprime l'effet de levier. La taille des points donne la distance de Cook, c'est à dire les points qui auront une influence (trop) grande sur le modèle.

On peut aussi visualiser la distance de Cook seule avec le graphique suivant point par point :

influence = myreg.get_influence() # myreg doit être un model de statsmodels

# c : distance et p : p-value

(c, p) = influence.cooks_distance

plt.stem(np.arange(len(c)), c, markerfmt=",")

plt.axhline(y=1,color="r",linewidth=1)

plt.axhline(y=4/len(Y),color="g",linewidth=1,linestyle="--")

plt.ylim(0,1.1) # Limites à éditer soi-même

plt.show()

Si on cite l'article Wikipedia sur le sujet (dont les sources sont bien indiquées) , on retiendra que les points trop influents franchissent une limite de Di = 1 (ligne rouge) ou Di = 4/n (ligne verte).

Étape 4 - Estimer la pertinence du modèle

On vient de voir de nombreux graphiques qui permettent de valider un modèle de régression.

Ce n'est pas toujours simples de les avoir sous python, alors, on peut émuler R pour les avoir grâce au module Rpy2.

Sinon, j'envisage de compiler une fonction valreg() qui pourrait faire toutes les validations de façon automatique. Ce serait pour le moins, pratique.

Étape 5 - Régression multiple

5.1- Approche simple en python

Faire une régression multiple suit la même logique qu'une régression simple.

Etape 1 - Définir les variables X

Option 1 - Définir les variables X que l'on veut dans la dataframe en supprimant celles que l'on ne veut pas avec drop()

list_var=compil.columns.drop(["imc","num","sexe"]) ; X=compil[list_var] ; print(X)

Option 2 - Définir les noms des variables X que l'on désire

X = compil.loc[:,["poids","taille"]] ; print(X)

Option 3 - Définir les variables X que l'on désire en les appelant par son index

X = compil.iloc[:,1:3] ; print(X)

Etape 2 - Définir la/les variables Y

Y = compil.imc

Etape 3 - Faire le modèle de régression

myreg = LinearRegression().fit(X,Y)

print('coef dir =',myreg.coef_, 'ord orgine =',myreg.intercept_)

5.2- Approche inspirée de la fonction lm() de R

  • Approche basique

import statsmodels.api as sm

mod = sm.OLS(compil.imc, compil.iloc[:,1:3])

# Ajuster le modèle

lmfit = mod.fit() # Résumé des principales

print(lmfit.summary())

  • Approche avec des formules

import statsmodels.api as sm

import statsmodels.formula.api as smf

print(compil) # données simulées dans le paragraphe 0 ci-dessus

# Définition du modèle

import statsmodels.formula.api as smf

model = smf.ols(formula='imc ~ poids + taille',data=compil)

myreg = model.fit()

print(myreg .summary())

  • Si message d'erreur : copier coller dans la console windows

pip install --upgrade patsy

pip install statsmodels --upgrade

pip install pandas --upgrade

OLS Regression Results ==============================================================================Dep. Variable: imc R-squared: 0.987Model: OLS Adj. R-squared: 0.987Method: Least Squares F-statistic: 6550.Date: Fri, 15 Nov 2019 Prob (F-statistic): 1.13e-166Time: 18:34:21 Log-Likelihood: -90.952No. Observations: 180 AIC: 187.9Df Residuals: 177 BIC: 197.5Df Model: 2 Covariance Type: nonrobust ============================================================================== coef std err t P>|t| [0.025 0.975]------------------------------------------------------------------------------Intercept 42.5206 0.492 86.367 0.000 41.549 43.492poids 0.3425 0.003 109.745 0.000 0.336 0.349taille -0.2487 0.003 -77.934 0.000 -0.255 -0.242==============================================================================Omnibus: 19.191 Durbin-Watson: 1.904Prob(Omnibus): 0.000 Jarque-Bera (JB): 67.664Skew: -0.222 Prob(JB): 2.03e-15Kurtosis: 5.971 Cond. No. 3.00e+03

Étape 6 - Sélectionner le meilleur modèle

Analyser certains paramètres tels l'AIC et le BIC qui doivent être le plus bas possible ! (cf. summary())

Avec statsmodels

myreg.bic

myreg.aic

Avec sklearn (à contrôler... )

from sklearn.linear_model import LassoLarsIC

model_bic = LassoLarsIC(criterion="bic", normalize=False)

model_bic.fit(X, y)

alpha_bic_ = model_bic.alpha_ # Coefficients de pénalité


model_aic = LassoLarsIC(criterion="aic", normalize=False)

model_aic.fit(X, y)

alpha_aic_ = model_aic.alpha_ # Coefficients de pénalité

Je n'ai pas pu encore fait une comparaison sur la solidité de l'approche sklearn, cette fonction étant adapté normalement à Lasso et Lars, à vérifier donc.

Étape 7 - Optimiser son modèle de régression

Rubrique en cours de développement. cf. la démarche sous R pour équivalence.

Étape 8 - Validation du modèle

Pour valider un modèle, il existe plusieurs approches :

  1. Validation avec échantillon d'apprentissage et échantillon test - on s'assure alors que le MSE soit bas. Et que le modèle marche pour l'échantillon test.

  2. Validation croisée : un bootstrap permet de refaire plusieurs fois la méthode ci-dessus.

Étape 9 - Faire des prédictions

  • Sous sklearn ou statsmodels

Y_pred = myreg.predict(X)

Y_pred = myreg.predict(compil[["poids","taille"]]) # Dans un exemple ou le modèle de prédiction de Y aurait été conçu à partir des variables poids et taille.

Étape 10 - Afficher des régressions multiples en modélisant des nappes

Reprenons les données du haut de la page et faisons une régression de l'IMC en fonction de deux variables X1 et X2 : la Taille et le Poids

Etape 1 - Récupérons à partir des deux variables des valeurs couvrant l'ensemble d'une grille

variable1 = compil.poids # à modifier

variable2 = compil.taille # à modifier

res = 40 # résolution à paramétrer soi-même : plus c'est grand, plus c'est joli mais lourd

X1 = np.arange(min(variable1 ),max(variable1 ),((max(variable1 )-min(variable1 ))/res))

X2 = np.arange(min(variable2 ),max(variable2 ),((max(variable2 )-min(variable2 ))/res))

Etape 2 -Je combine les deux étendues de X1 et X2 pour créer une Grille de valeurs possibles

# Une fonction utile

def expand_grid(x, y,name_x1="X1",name_x2="X2"):

x1, x2 = np.meshgrid(x, y) # create the actual grid

x1G = x1.flatten() # make the grid 1D

x2G = x2.flatten() # conversion 1D

return x1, x2, pd.DataFrame({name_x1:x1G,name_x2:x2G})

X1, X2, Grille=expand_grid(X1,X2,name_x1="poids",name_x2="taille")

Etape 3 - J'utilise la commande predict() pour simuler toutes les valeurs d'IMC possible pour les combinaisons de X1 et X2

out = myreg.predict(Grille)

# Je remets en 2D les prédictions

out = np.array(out).reshape(res,res)

Etape 4 -J'affiche le graphique en 3D

from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()

ax = fig.gca(projection='3d')

surface = ax.plot_surface(X1,X2,out, rstride=1, cstride=1, cmap="plasma",

linewidth=0, antialiased=False)

ax.set_zlim(17, 30) # A paramétrer

fig.colorbar(surface, shrink=0.5, aspect=5)

plt.show()

Une source externe pour faire une comparaison entre R et python : source externe.