import warnings
def fxn():
warnings.warn("deprecated", DeprecationWarning)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fxn()
# importations de modules "local"
import sys
sys.path.append('functions/')
import base_notebook_light
import css_base_light
import fonctions_perso
import css_regression
import fonction_regression_lineaire
# import des librairies et des paramètres
from base_notebook_light import *
from css_base_light import *
from css_regression import *
# style pour les graphiques
plt.style.use('seaborn-notebook')
# repère de début de traitement pour calculer le temps tital d'exécution du notebook
start_time_m4 = time.time()
# import des données
dcf = pd.read_csv("data/exports/dc_anova.csv")
dcf.shape
(5750000, 11)
dcf = dcf[['code_pays', 'pays', 'pop', 'Gj', 'gdp', 'income_avg', 'y_child', 'pj', 'c_i_parent']]
dcf.columns = ['code_pays', 'pays', 'pop', 'gini', 'gdp', 'y_child_avg', 'y_child', 'elas', 'c_i_parent']
dcf.sample(2)
code_pays | pays | pop | gini | gdp | y_child_avg | y_child | elas | c_i_parent | |
---|---|---|---|---|---|---|---|---|---|
2157539 | HUN | Hongrie | 9991867.0 | 0.2741 | 18004.0 | 6101.341229 | 3279.2130 | 0.400000 | 5.0 |
1201982 | CZE | République Tchèque | 10425266.0 | 0.2529 | 23223.0 | 8235.293411 | 3067.3618 | 0.434041 | 80.0 |
dcf['ln_y_child'] = np.log(dcf['y_child'])
dcf['ln_y_child_avg'] = np.log(dcf['y_child_avg'])
dcf.sample(2)
code_pays | pays | pop | gini | gdp | y_child_avg | y_child | elas | c_i_parent | ln_y_child | ln_y_child_avg | |
---|---|---|---|---|---|---|---|---|---|---|---|
5244595 | TUR | Turquie | 70418604.0 | 0.4220 | 11904.0 | 6050.465331 | 11233.96100 | 0.5 | 40.0 | 9.326697 | 8.707890 |
2956745 | LAO | République Démocratique Populaire Lao | 6046620.0 | 0.3542 | 1960.0 | 1003.407353 | 450.35175 | 0.5 | 29.0 | 6.110029 | 6.911157 |
dcf['pop'] = dcf['pop'].apply(lambda x: int(x))
dcf['pop'] = dcf['pop'].apply(lambda x: "{:,}".format(x).replace(",", " "))
dcf['gdp'] = dcf['gdp'].apply(lambda x: round(x,2))
dcf['y_child'] = dcf['y_child'].apply(lambda x: round(x,2))
dcf['y_child_avg'] = dcf['y_child_avg'].apply(lambda x: round(x,2))
dcf['elas'] = dcf['elas'].apply(lambda x: round(x,2))
dcf['c_i_parent'] = dcf['c_i_parent'].apply(lambda x: int(x))
dcf.sample(2)
code_pays | pays | pop | gini | gdp | y_child_avg | y_child | elas | c_i_parent | ln_y_child | ln_y_child_avg | |
---|---|---|---|---|---|---|---|---|---|---|---|
2353237 | IRN | République Islamique d'Iran | 72 120 604 | 0.4344 | 10446.0 | 5832.66 | 1240.29 | 0.66 | 19 | 7.123098 | 8.671228 |
1808996 | GEO | Géorgie | 4 142 655 | 0.3901 | 4516.0 | 1363.76 | 537.60 | 0.50 | 96 | 6.287116 | 7.218000 |
code_pays
: code international iso-3pays
: nom du pays en françaispop
: nombre d'habitants du paysgini
: indice de gini du paysgdp
: PIB/hab en $PPAy_child_avg
: revenu moyen des enfantsy_child
: revenu enfant par centile c_i_parentelas
: coef d'élasticité du paysc_i_parent
: centile de revenu parentln_y_child
: logarithme de y_childln_y_child_avg
: logarithme de y_child_avgc_i_parent
=> les mêmes observations sont donc dupliquées 500 fois # version allégée pour les graphs
dcf_graph = dcf.drop(columns=["c_i_parent"]).drop_duplicates()
dcf_graph.shape
(11500, 10)
Représentation graphique pour les 6 pays retenus lors de la mission 2 :
Slovénie, Honduras, Géorgie, États-Unis, Ukraine, Paraguay
pays_selected = ['Slovénie', 'Honduras', 'Géorgie', 'États-Unis', 'Ukraine', 'Paraguay']
pays_sel_l = []
for pays in pays_selected:
pays_sel_l.append(dcf[dcf['pays'] == pays])
dcf_sel = pd.concat(pays_sel_l)
dcf_sel.shape
dcf_sel.sample(4)
(300000, 11)
code_pays | pays | pop | gini | gdp | y_child_avg | y_child | elas | c_i_parent | ln_y_child | ln_y_child_avg | |
---|---|---|---|---|---|---|---|---|---|---|---|
2098933 | HND | Honduras | 7 980 955 | 0.6017 | 3628.0 | 3296.27 | 15031.88 | 0.66 | 98 | 9.617929 | 8.100546 |
1842366 | GEO | Géorgie | 4 142 655 | 0.3901 | 4516.0 | 1363.76 | 2160.37 | 0.50 | 85 | 7.678035 | 7.218000 |
5394913 | UKR | Ukraine | 46 158 711 | 0.2551 | 6721.0 | 3349.39 | 5321.15 | 0.40 | 92 | 8.579444 | 8.116533 |
1819655 | GEO | Géorgie | 4 142 655 | 0.3901 | 4516.0 | 1363.76 | 889.74 | 0.50 | 29 | 6.790935 | 7.218000 |
sns.set()
sns.set_theme(context='notebook', style='whitegrid', palette='Set2', font_scale=1)
plt.figure(figsize=(8,5))
sns.boxplot(x="pays", y="y_child", data=dcf_sel, flierprops=dict(marker='o', markersize=3))
plt.xlabel("", size=0)
plt.ylabel("Revenu (en $PPA)", size=12)
plt.title("Répartition des revenus dans les pays sélectionnés (mission 2)", fontsize=14, color="darkred")
plt.grid(True, linestyle='--', alpha=0.3)
plt.tight_layout()
plt.savefig('data/exports/img_charts/12.boxplot_sns_revenus_pays_selected.png', dpi = 200)
plt.show();
import plotly.graph_objects as go
def plot_box_plot(pays_l, var_x, num_graph=None):
x_data = pays_l
y_data=[]
for pays in pays_l:
y_data.append(dcf_graph.loc[dcf_graph['pays'] == pays, var_x])
colors = ['rgba(93, 164, 214, 0.5)', 'rgba(255, 144, 14, 0.5)', 'rgba(44, 160, 101, 0.5)',
'rgba(255, 65, 54, 0.5)', 'rgba(207, 114, 255, 0.5)', 'rgba(127, 96, 0, 0.5)']
fig = go.Figure()
for xd, yd, cls in zip(x_data, y_data, colors):
fig.add_trace(go.Box(
y=yd,
name=xd,
boxpoints='all',
jitter=0.5,
whiskerwidth=0.2,
fillcolor=cls,
marker_size=2,
line_width=1)
)
fig.update_layout(
title='Répartition des revenus',
yaxis=dict(
autorange=True,
showgrid=True,
zeroline=True,
gridcolor='rgb(255, 255, 255)',
gridwidth=1,
zerolinecolor='rgb(255, 255, 255)',
zerolinewidth=2,
),
margin=dict(
l=40,
r=30,
b=80,
t=100,
),
paper_bgcolor='rgb(243, 243, 243)',
plot_bgcolor='rgb(243, 243, 243)',
showlegend=False
)
fig.write_html("data/exports/img_charts/"+(str(num_graph) if num_graph is not None else "")+"boxplot_plot_revenus_pays_selected.html")
fig.write_image("data/exports/img_charts/"+str((num_graph) if num_graph is not None else "")+"boxplot_plot_revenus_pays_selected.png")
fig.show();
plot_box_plot(pays_selected, 'y_child', num_graph=13)
Les Etats-Unis ont des valeurs extrèmes très importantes, "écrasant" ainsi les distributions des autres pays.
=> on réalise un nouveau graphique en excluant les Etats-Unis
pays_selected_sans_eu = list(pays_selected)
pays_selected_sans_eu.remove('États-Unis')
plot_box_plot(pays_selected_sans_eu, 'y_child', num_graph=14)
Si la p-value est inférieure à 0,05, nous rejetons l'hypothèse nulle en faveur de l'alternative :
-> cela signifie qu'au moins une moyenne de groupe est significativement différente.
- Le test d'ANOVA (analyse de la variance) :
- Test F qui permet de savoir si nos résultats sont significatifs, et donc si on peut rejet l'hypothèse nulle H0 au profit de l'hypothèse alternative H1
- calcul de eta² (η²) afin de savoir si notre variable qualitative et notre variable quantitative sont corrélées (cf. la proportion de la variance totale expliquée par le modèle)
Le résultat obtenu est compris entre 0 et 1. Plus il est proche de 1 plus les données sont corrélées.
Interprétation de η² (cf la corrélation) :
* si η² = 0 alors absence de corrélation entre les variables X et Y
* plus η² est proche de 1, plus la corrélation entre les variables X et Y est importante
statsmodels
pour réaliser l'ANOVAanova_pays = ols('y_child ~ code_pays', data=dcf).fit()
display(anova_pays.summary().tables[0])
Dep. Variable: | y_child | R-squared: | 0.497 |
---|---|---|---|
Model: | OLS | Adj. R-squared: | 0.497 |
Method: | Least Squares | F-statistic: | 4.986e+04 |
Date: | Fri, 18 Jun 2021 | Prob (F-statistic): | 0.00 |
Time: | 20:18:56 | Log-Likelihood: | -5.8752e+07 |
No. Observations: | 5750000 | AIC: | 1.175e+08 |
Df Residuals: | 5749885 | BIC: | 1.175e+08 |
Df Model: | 114 | ||
Covariance Type: | nonrobust |
table = sm.stats.anova_lm(anova_pays)
display(table)
df | sum_sq | mean_sq | F | PR(>F) | |
---|---|---|---|---|---|
code_pays | 114.0 | 2.496311e+14 | 2.189746e+12 | 49864.239021 | 0.0 |
Residual | 5749885.0 | 2.525014e+14 | 4.391416e+07 | NaN | NaN |
=> le pays a donc une influence sur les montants de revenus
=> eta carré (η²) indique la part de la variance totale expliquée par notre variable pays
esq_sm = table['sum_sq'][0]/(table['sum_sq'][0]+table['sum_sq'][1])
table_eta = table.copy()
table_eta['EtaSq'] = [esq_sm, 'NaN']
display(table_eta)
df | sum_sq | mean_sq | F | PR(>F) | EtaSq | |
---|---|---|---|---|---|---|
code_pays | 114.0 | 2.496311e+14 | 2.189746e+12 | 49864.239021 | 0.0 | 0.497142 |
Residual | 5749885.0 | 2.525014e+14 | 4.391416e+07 | NaN | NaN | NaN |
η² $\simeq$ 0.5
Test de Fisher repose sur les hypothèses suivantes :
égalité des variances des distributions
=> on peut tester ces hypothèses de la façon suivante :
Mais ANOVA est robuste à ces conditions d'applications
-> ainsi, si les échantillons sont de tailles suffisantes (ce qui est ici le cas avec 100 valeurs différentes par pays), on peut interpréter les résultats d'une ANOVA
Definition (résidus = erreurs obsevées):
Pour tester la normalité de distribution des revenus, nous allons tester la normalité des résidus de notre modèle :
# Représentation graphique de la distribution des résidus
X_res = anova_pays.resid
#plt.style.use('seaborn')
plt.figure(figsize=(10,10))
# graph 1 : qqplot des résidus
plt.subplot(211)
st.probplot(X_res, dist="norm", plot= plt)
plt.title('Q-Q plot', y=0.9, color="darkred")
plt.grid(True, linestyle='--', alpha=0.3)
# graph 2 : distribution des résidus
plt.subplot(212)
sns.histplot(x=X_res, kde=True)
plt.title(f'Distribution des residus', y=0.9, color="darkred")
plt.ylabel('Nombre')
plt.xlabel('Residus')
plt.grid(True, linestyle='--', alpha=0.3)
plt.suptitle("Analyse des résidus - Modèle linéaire (Anova à 1 facteur : le pays)", fontsize=14)
plt.tight_layout(h_pad=2.5)
path_graph = "data/exports/img_anova_1_facteur/"
os.makedirs(path_graph, exist_ok=True)
plt.savefig(path_graph+"1.mod_lineaire_analyse_residus.png", dpi = 200)
plt.show();
import statsmodels.stats.diagnostic as smd
stat, p = smd.kstest_normal(anova_pays.resid, dist='norm')
print(f"p_value : {p}")
if p < 0.05:
print("La p_value est inférieure au seuil de 5%, donc on rejette HO : les résidus ne suivent pas une loi normale")
else:
print("La p_value est supérieure au seuil de 5%, donc on ne peut pas rejeter HO : les résidus suivent une loi normale")
p_value : 0.0009999999999998899 La p_value est inférieure au seuil de 5%, donc on rejette HO : les résidus ne suivent pas une loi normale
Le test de normalité des résidus n'étant pas concluant, nous allons utiliser le test de Levene
pour tester la constance des variances
import pingouin as pg
res_anova_homo = pg.homoscedasticity(dcf, dv='y_child', group='code_pays', method='levene')
display(res_anova_homo)
W | pval | equal_var | |
---|---|---|---|
levene | 12753.68435 | 0.0 | False |
P_value = 0
=> on rejette l'hypothèse H0 d'homoscédasticité des variances
display(HTML(
'<div style="border:1px; border-color:gray; border-style:solid; border-radius:4px; width:max-content; padding:10px; margin-left: auto; margin-right: auto;margin-top: 10px;">'+
'<div style="text-align:center; font-size:120%; color:chocolate">Conditions d\'application de l\'ANOVA</div>'+
'<ul>'+
'<li>Les résidus, et donc nos données, ne suivent pas une loi normale</li>'+
'<li>Hétéroscédasticité des variances</li>'+
'</div>'
))
# Test avec log(revenu)
anova_pays_ln = ols('ln_y_child ~ code_pays', data=dcf).fit()
display(anova_pays_ln.summary().tables[0])
Dep. Variable: | ln_y_child | R-squared: | 0.727 |
---|---|---|---|
Model: | OLS | Adj. R-squared: | 0.727 |
Method: | Least Squares | F-statistic: | 1.340e+05 |
Date: | Fri, 18 Jun 2021 | Prob (F-statistic): | 0.00 |
Time: | 20:22:40 | Log-Likelihood: | -6.2697e+06 |
No. Observations: | 5750000 | AIC: | 1.254e+07 |
Df Residuals: | 5749885 | BIC: | 1.254e+07 |
Df Model: | 114 | ||
Covariance Type: | nonrobust |
En utilisant le logarithme des revenus, on constate que :
X_res = anova_pays_ln.resid
sns.color_palette("Set2")
plt.figure(figsize=(10,10))
# graph 1 : qqplot des résidus
plt.subplot(211)
st.probplot(X_res, dist="norm", plot= plt)
plt.title('Q-Q plot', y=0.9, color="darkred")
plt.grid(True, linestyle='--', alpha=0.3)
# graph 2 : distribution des résidus
plt.subplot(212)
sns.histplot(x=X_res, kde=True)
plt.title(f'Distribution des residus', y=0.9, color="darkred")
plt.ylabel('Nombre')
plt.xlabel('Residus')
plt.grid(True, linestyle='--', alpha=0.3)
plt.suptitle("Analyse des résidus - Modèle log (Anova à 1 facteur : le pays)", fontsize=14)
plt.tight_layout(h_pad=2.5)
path_graph = "data/exports/img_anova_1_facteur/"
os.makedirs(path_graph, exist_ok=True)
plt.savefig(path_graph+"2.mod_log_analyse_residus.png", dpi = 200)
plt.savefig('data/exports/img_anova_1_facteur/2.mod_log_analyse_residus.png', dpi = 200)
plt.show();
stat_ln, p_ln = smd.kstest_normal(anova_pays_ln.resid, dist='norm')
print(f"p_value : {p_ln}")
if p < 0.05:
print("La p_value est inférieure au seuil de 5%, donc on rejette HO : les résidus ne suivent pas une loi normale")
else:
print("La p_value est supérieure au seuil de 5%, donc on ne peut pas rejeter HO : les résidus suivent une loi normale")
p_value : 0.0009999999999998899 La p_value est inférieure au seuil de 5%, donc on rejette HO : les résidus ne suivent pas une loi normale
import pingouin as pg
res_anova_homo_ln = pg.homoscedasticity(dcf, dv='ln_y_child', group='code_pays', method='levene')
display(res_anova_homo_ln)
W | pval | equal_var | |
---|---|---|---|
levene | 4840.231236 | 0.0 | False |
P_value = 0
=> on rejette l'hypothèse H0 d'homoscédasticité des variances
Nous allons choisir le modèle plus performant, c'est-à-dire celui dont la variance expliquée est la plus importante :
-> celui dont le $R^2$ est le plus élevé
Equation du modèle dans le cadre linéaire :
$$\large \text{y_child}_j\: = \: \beta_0\: +\: \beta_1\, \text{y_child_avg}_j\: +\: \beta_2\, \text{gini}_j\: +\: \epsilon_j$$
Equation du modèle dans le cadre logarithmique :
$$\large \text{log(y_child)}_j\: = \: \beta_0\: +\: \beta_1\, \text{log(y_child_avg)}_j\: +\: \beta_2\, \text{gini}_j\: +\: \epsilon_j$$
avec :
# on régresse y_child en fonction de 'y_child_avg' et de 'gini'
reg_multi = ols('y_child ~ y_child_avg+gini', data=dcf).fit()
# on régresse y_child en fonction de 'y_child_avg' et de 'gini'
y = dcf['y_child']
X = dcf[['y_child_avg', 'gini']]
X = sm.add_constant(X)
reg_multi = sm.OLS(y,X).fit()
#reg_multi = ols('y_child ~ y_child_avg+gini', data=dcf).fit()
print(reg_multi.summary())
OLS Regression Results ============================================================================== Dep. Variable: y_child R-squared: 0.497 Model: OLS Adj. R-squared: 0.497 Method: Least Squares F-statistic: 2.842e+06 Date: Fri, 18 Jun 2021 Prob (F-statistic): 0.00 Time: 20:24:06 Log-Likelihood: -5.8752e+07 No. Observations: 5750000 AIC: 1.175e+08 Df Residuals: 5749997 BIC: 1.175e+08 Df Model: 2 Covariance Type: nonrobust =============================================================================== coef std err t P>|t| [0.025 0.975] ------------------------------------------------------------------------------- const -0.0022 13.945 -0.000 1.000 -27.334 27.329 y_child_avg 1.0000 0.000 2233.299 0.000 0.999 1.001 gini 0.0049 32.934 0.000 1.000 -64.544 64.554 ============================================================================== Omnibus: 7304031.881 Durbin-Watson: 0.001 Prob(Omnibus): 0.000 Jarque-Bera (JB): 2216400965.664 Skew: 6.841 Prob(JB): 0.00 Kurtosis: 98.204 Cond. No. 1.15e+05 ============================================================================== Notes: [1] Standard Errors assume that the covariance matrix of the errors is correctly specified. [2] The condition number is large, 1.15e+05. This might indicate that there are strong multicollinearity or other numerical problems.
reg_multi.summary().tables[0]
Dep. Variable: | y_child | R-squared: | 0.497 |
---|---|---|---|
Model: | OLS | Adj. R-squared: | 0.497 |
Method: | Least Squares | F-statistic: | 2.842e+06 |
Date: | Fri, 18 Jun 2021 | Prob (F-statistic): | 0.00 |
Time: | 20:24:06 | Log-Likelihood: | -5.8752e+07 |
No. Observations: | 5750000 | AIC: | 1.175e+08 |
Df Residuals: | 5749997 | BIC: | 1.175e+08 |
Df Model: | 2 | ||
Covariance Type: | nonrobust |
La p_value du test de Fischer est égale à 0
==> le modèle est donc significatif, au moins un coefficient est non nul
reg_multi.summary().tables[1]
coef | std err | t | P>|t| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
const | -0.0022 | 13.945 | -0.000 | 1.000 | -27.334 | 27.329 |
y_child_avg | 1.0000 | 0.000 | 2233.299 | 0.000 | 0.999 | 1.001 |
gini | 0.0049 | 32.934 | 0.000 | 1.000 | -64.544 | 64.554 |
Variable "revenu moyen"
H1 : $\beta_1 \ne 0$
=> p_value = 0 => la variable 'revenu moyen' est donc significative
Variable "indice de gini"
H1 : $\beta_2 \ne 0$
=> p_value = 1, donc supérieure à 0.05 => la variable 'indice de gini' n'est pas significative
On constate que le coef de détermination $R^2$ est égal à environ 0.5, ce qui correspond à la valeur du η² obtenu précédemment avec l'ANOVA à un facteur
=> résultat cohérent
On va effectuer une nouvelle régression linéaire, en utilisant les logarithmes
# on régresse ln_y_child en fonction de 'ln_y_child_avg' et de 'gini'
reg_multi_ln = ols('ln_y_child ~ ln_y_child_avg+gini', data=dcf).fit()
display(reg_multi_ln.summary())
Dep. Variable: | ln_y_child | R-squared: | 0.726 |
---|---|---|---|
Model: | OLS | Adj. R-squared: | 0.726 |
Method: | Least Squares | F-statistic: | 7.623e+06 |
Date: | Fri, 18 Jun 2021 | Prob (F-statistic): | 0.00 |
Time: | 20:24:09 | Log-Likelihood: | -6.2743e+06 |
No. Observations: | 5750000 | AIC: | 1.255e+07 |
Df Residuals: | 5749997 | BIC: | 1.255e+07 |
Df Model: | 2 | ||
Covariance Type: | nonrobust |
coef | std err | t | P>|t| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
Intercept | 0.4648 | 0.003 | 162.145 | 0.000 | 0.459 | 0.470 |
ln_y_child_avg | 0.9861 | 0.000 | 3618.416 | 0.000 | 0.986 | 0.987 |
gini | -1.6350 | 0.003 | -470.506 | 0.000 | -1.642 | -1.628 |
Omnibus: | 369335.484 | Durbin-Watson: | 0.001 |
---|---|---|---|
Prob(Omnibus): | 0.000 | Jarque-Bera (JB): | 1732751.061 |
Skew: | -0.083 | Prob(JB): | 0.00 |
Kurtosis: | 5.684 | Cond. No. | 113. |
La p_value du test de Fischer est égale à 0
==> le modèle est donc significatif, au moins un coefficient est non nul
Les p_value du test de Student pour les 2 variables explicatices "revenu moyen" et "indice de Gini" sont égales à 0 :
==> Les 2 variables sont donc significatives
Le coefficient de détermination réprésente la part de la variation totale de y qui est expliquée par le modèle : $R^2 = \frac{SCE} {SCT}$
avec :
anova_reg_2f_ln =sm.stats.anova_lm(reg_multi_ln)
anova_reg_2f_ln
df | sum_sq | mean_sq | F | PR(>F) | |
---|---|---|---|---|---|
ln_y_child_avg | 1.0 | 7.800125e+06 | 7.800125e+06 | 1.502387e+07 | 0.0 |
gini | 1.0 | 1.149342e+05 | 1.149342e+05 | 2.213755e+05 | 0.0 |
Residual | 5749997.0 | 2.985295e+06 | 5.191821e-01 | NaN | NaN |
r2 = (anova_reg_2f_ln.sum_sq['ln_y_child_avg'] + anova_reg_2f_ln.sum_sq['gini']) / anova_reg_2f_ln.sum_sq.sum()
print("On a bien R² égal à "+str(round(r2,5)))
On a bien R² égal à 0.72613
Déterminons les paramètres de départ
# seuil retenu
alpha = 0.05
# taille échantillon
n = dcf.shape[0]
# nb de variables
p = 2
# création d'un df analyses
analyses = dcf.copy()
analyses['obs'] = analyses.index +1
Définition
La distance utilisée est la distance de Mahalanobis
==> Le levier $h_i$ de l'observation i est lu sur la diagonale principale de la matrice H, dite Hat Matrix, définie de la manière suivante :
$$ H = X {(X'X)}^{-1} X'$$
Région critique
cf. le seuil qui indique qu'un point est suspect
le seuil des leviers retenu est le suivant : $2\times\frac{\displaystyle p+1}{\displaystyle n}$
# creation d'une instance d'Influence
infl = reg_multi_ln.get_influence()
# insert des leviers dans le df analyses
analyses['levier'] = infl.hat_matrix_diag
# seuil des leviers
seuil_levier = 2*((p+1)/n)
print("Seuil des leviers : {:.10f}".format(float(seuil_levier)))
analyses.sample(10)
analyses.shape
Seuil des leviers : 0.0000010435
code_pays | pays | pop | gini | gdp | y_child_avg | y_child | elas | c_i_parent | ln_y_child | ln_y_child_avg | obs | levier | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1188572 | CYP | Chypre | 1 081 563 | 0.2805 | 26273.00 | 17345.39 | 20874.58 | 0.34 | 25 | 9.946288 | 9.761082 | 1188573 | 6.422852e-07 |
2126886 | HRV | Croatie | 4 352 636 | 0.3028 | 17219.00 | 7716.47 | 6828.32 | 0.46 | 77 | 8.828834 | 8.951112 | 2126887 | 3.532738e-07 |
433926 | BGR | Bulgarie | 7 524 087 | 0.3619 | 11993.00 | 4984.98 | 5376.69 | 0.40 | 87 | 8.589828 | 8.514184 | 433927 | 1.993626e-07 |
4485518 | PRY | Paraguay | 6 081 296 | 0.5251 | 4347.00 | 3278.08 | 3262.92 | 0.66 | 12 | 8.090378 | 8.095013 | 4485519 | 6.670162e-07 |
2557261 | ITA | Italie | 58 922 109 | 0.3173 | 28170.00 | 14925.21 | 6879.40 | 0.49 | 35 | 8.836287 | 9.610807 | 2557262 | 5.032233e-07 |
3711595 | MRT | Mauritanie | 3 296 238 | 0.4043 | 2226.73 | 1798.61 | 810.78 | 0.66 | 14 | 6.697994 | 7.494769 | 3711596 | 2.253665e-07 |
3846044 | MYS | Malaisie | 27 236 006 | 0.4682 | 13163.00 | 6006.34 | 14071.04 | 0.54 | 33 | 9.551874 | 8.700571 | 3846045 | 4.608026e-07 |
913070 | CIV | Côte d'Ivoire | 19 605 569 | 0.4149 | 1526.00 | 399.84 | 189.57 | 0.66 | 11 | 5.244780 | 5.991052 | 913071 | 7.646059e-07 |
679333 | BTN | Bhoutan | 671 613 | 0.3808 | 4525.48 | 1515.93 | 1342.30 | 0.50 | 69 | 7.202140 | 7.323784 | 679334 | 2.578214e-07 |
1013609 | COD | République Démocratique du Congo | 60 411 195 | 0.4440 | 303.19 | 276.02 | 122.53 | 0.71 | 17 | 4.808340 | 5.620459 | 1013610 | 9.936776e-07 |
(5750000, 13)
# représentation graphique des leviers
matplotlib.rcdefaults()
plt.style.use('seaborn-deep')
fig = plt.figure(figsize=(10,5))
ax = fig.gca()
ax.yaxis.set_major_formatter(FormatStrFormatter('%.7f'))
values = [1000000*i for i in range(7)]
labels = ["0", "1 Million", "2 Millions", "3 Millions", "4 Millions", "5 Millions", "6 Millions"]
ax.set_xticks(values)
ax.set_xticklabels(labels)
plt.xlabel('Observations')
plt.ylabel('Leviers')
plt.title("Valeurs des leviers", fontsize=14, color="darkred")
ax.plot(analyses['levier'])
plt.axhline(y=seuil_levier, color='red', linestyle='-')
plt.grid(True, linestyle='--', alpha=0.3)
plt.tight_layout()
path_graph = "data/exports/img_regression/2_var_log/"
os.makedirs(path_graph, exist_ok=True)
plt.savefig(path_graph+"2_var_log_5_leviers_glob.png", dpi = 200)
plt.show();
# dict qui contiendra les différents résultats
synthese_res = {}
# Nombre de valeurs et pays concernés
res_leviers = analyses.loc[analyses['levier'] > seuil_levier, ['pays', 'ln_y_child_avg', 'levier']].groupby('pays').size()
synthese_res['pays_sup_leviers'] = res_leviers
res_leviers
pays Afrique du Sud 50000 Honduras 50000 dtype: int64
pays_supects = analyses.loc[analyses['levier'] > seuil_levier, ['pays']].drop_duplicates()
print("Ainsi, les valeurs des leviers des 2 pays suivants sont atypiques:")
for i in pays_supects.pays:
print(f" - {i}")
Ainsi, les valeurs des leviers des 2 pays suivants sont atypiques: - Honduras - Afrique du Sud
Remarque :
Il est normal que le nombre de valeurs soit de 50000.
En effet, la notion de levier fait référence uniquement aux variables explicatives, la variable cible (ici les revenus), n'est pas prise en compte.
Or étant donné la structure de nos données, il n'y a qu'une valeur de levier par pays (car un seul revenu moyen et un seul indice de Gini par pays) : c'est donc 50000 fois la même valeur
# pays avec des leviers importants
pays_leviers = analyses.groupby('pays')['levier'].max().sort_values(ascending=False)
synthese_res['pays_leviers_importants'] = pays_leviers
pays_leviers.head(10).apply(lambda x: '{:.8f}'.format(float(x)))
pays Afrique du Sud 0.00000232 Honduras 0.00000132 Colombie 0.00000103 République Démocratique du Congo 0.00000099 République Centrafricaine 0.00000098 Guatemala 0.00000095 Kenya 0.00000095 États-Unis 0.00000094 Bolivie 0.00000093 Chili 0.00000091 Name: levier, dtype: object
Le résidu standardisé (ou résidu studentisé interne)
Région critique
cf. le seuil qui indique qu'un point est suspect :
le seuil des résidus studentisés est : $|t_i| > t_{1 - \frac {\alpha}{2}}(n-p-1) $
avec $t_{1 - \frac {\alpha}{2}}(n-p-1) $ le fractile d'ordre $1 - \frac {\alpha}{2}$ à $(n-p-1)$ degrés de libertés
# creation d'une instance d'Influence
infl = reg_multi_ln.get_influence()
# insert des résidus studentisé dans le df analyses
analyses['res_stud'] = infl.resid_studentized_internal
# seuil résidus studentisés
from scipy.stats import t
seuil_stud = t.ppf(1-alpha/2,n-p-1)
print('Seuil des résidus standardisés : {:.4f}'.format(float(seuil_stud)))
Seuil des résidus standardisés : 1.9600
analyses.sample(5)
code_pays | pays | pop | gini | gdp | y_child_avg | y_child | elas | c_i_parent | ln_y_child | ln_y_child_avg | obs | levier | res_stud | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
3717027 | MRT | Mauritanie | 3 296 238 | 0.4043 | 2226.73 | 1798.61 | 1031.76 | 0.66 | 6 | 6.939024 | 7.494769 | 3717028 | 2.253665e-07 | -0.354680 |
5532195 | VEN | Venezuela | 27 635 832 | 0.4340 | 11756.00 | 3167.15 | 3049.87 | 0.66 | 47 | 8.022854 | 8.060587 | 5532196 | 2.413768e-07 | 0.442519 |
4738611 | SLV | El Salvador | 6 131 764 | 0.4749 | 6270.00 | 2855.22 | 3553.51 | 0.66 | 42 | 8.175690 | 7.956906 | 4738612 | 3.757340e-07 | 0.889337 |
2450065 | ISL | Islande | 310 856 | 0.2851 | 36527.00 | 26888.51 | 5191.97 | 0.20 | 5 | 8.554868 | 10.199454 | 2450066 | 8.260602e-07 | -2.084263 |
884488 | CHN | Chine | 1 383 985 631 | 0.4783 | 5712.00 | 2522.76 | 2793.85 | 0.40 | 98 | 7.935177 | 7.833108 | 884489 | 3.860249e-07 | 0.732687 |
# représentation graphique des résidus standardisés
fig = plt.figure(figsize=(10,5))
ax = fig.gca()
values = [1000000*i for i in range(7)]
labels = ["0", "1 Million", "2 Millions", "3 Millions", "4 Millions", "5 Millions", "6 Millions"]
ax.set_xticks(values)
ax.set_xticklabels(labels)
plt.xlabel('Observations')
plt.ylabel('Résidus Studentisés')
plt.title("Valeurs des résidus standardisés", fontsize=14, color="darkred")
plt.plot(analyses['res_stud'], zorder=2)
plt.axhline(y=-seuil_stud, color='red', linestyle='-', zorder=3)
plt.axhline(y=seuil_stud, color='red', linestyle='-', zorder=3)
plt.grid(True, linestyle='--', alpha=0.3)
plt.tight_layout()
path_graph = "data/exports/img_regression/2_var_log/"
os.makedirs(path_graph, exist_ok=True)
plt.savefig(path_graph+"2_var_log_6_residus_glob.png", dpi = 200)
plt.show();
# Nombre de valeurs et pays concernés
df_temp = analyses.loc[(analyses['res_stud'] < -seuil_stud) | (analyses['res_stud'] > seuil_stud), ['pays', 'ln_y_child_avg', 'res_stud']]
res_res_stud = df_temp.groupby('pays').size()
nb_res_stud = res_res_stud.sum()
nb_pays_avec_res_stud = len(res_res_stud)
part_res_stud = round(100 * nb_res_stud / analyses.shape[0],2)
print(f"Nombre de résidus dans la région critique : {nb_res_stud}, soit {part_res_stud}% du dataset")
print(f"{nb_pays_avec_res_stud} pays concernés (affichage des 10 pays présentant le plus de valeurs hors seuil)")
res_stud = res_res_stud.sort_values(ascending=False)
synthese_res['pays_res_stud'] = res_stud
res_stud.head(10)
Nombre de résidus dans la région critique : 316500, soit 5.5% du dataset 113 pays concernés (affichage des 10 pays présentant le plus de valeurs hors seuil)
pays Afrique du Sud 14500 Honduras 11000 Bolivie 10000 Colombie 8500 Panama 8000 Brésil 7500 Guatemala 7000 République Centrafricaine 7000 Paraguay 6500 Nicaragua 6500 dtype: int64
113 pays sur 115 ont des résidus standardisés supérieurs au seuil critique (en valeur absolue)
--> donc pour 2 pays, les erreurs du modèle sont en-dessous du seuil
---> déterminons quels sont ces pays
l_pays_avec_res = res_res_stud.index.tolist()
l_pays_total = list(set(analyses['pays'].tolist()))
x=set(l_pays_total)-set(l_pays_avec_res)
x_str = " et ".join(x)
print("Les 2 pays sans résidus standardisés hors seuil sont les suivants : "+x_str)
Les 2 pays sans résidus standardisés hors seuil sont les suivants : Bélarus et Ukraine
def graph_res_stud(pays):
df = analyses.loc[analyses['pays'] == pays].drop(columns=['obs', 'c_i_parent']).drop_duplicates().reset_index(drop=True)
df_in = df[(df['res_stud'] >= -seuil_stud) & (df['res_stud'] <= seuil_stud)]
df_out = df[(df['res_stud'] < -seuil_stud) | (df['res_stud'] > seuil_stud)]
if df_out.shape[0] != 0:
y_max = 1.1 * df_out['res_stud'].max()
y_min = 1.1 * df_out['res_stud'].min()
else:
y_max = 1.3 * seuil_stud
y_min = -1.3 * seuil_stud
plt.figure(figsize=(10,5))
x_in = df_in.index+1
y_in =df_in['res_stud']
x_out = df_out.index+1
y_out =df_out['res_stud']
plt.bar(x_in, y_in, color="green")
plt.bar(x_out, y_out, color="red", zorder=3)
plt.xlim(-0.05*df.shape[0], 1.05*df.shape[0])
plt.ylim(y_min, y_max)
plt.xlabel("observations")
plt.ylabel("résidus studentisés")
plt.title("Répartition des résidus studentisés pour le pays \""+str(pays)+"\"", color="darkred")
plt.text(0.2, seuil_stud + 0.05 * y_max, 'Seuil Résidus Student ({:.4f}'.format(float(seuil_stud))+')', fontsize = '10', color='teal')
plt.text(0.7 * df.shape[0], -seuil_stud + 0.1 * y_min, 'Seuil Résidus Student (-{:.4f}'.format(float(seuil_stud))+')', fontsize = '10', color='teal')
plt.axhline(y=-seuil_stud, color='teal', linestyle='--', zorder=3)
plt.axhline(y=seuil_stud, color='teal', linestyle='--', zorder=3)
plt.axhline(y=0, color='black', linestyle='-')
plt.grid(True, linestyle='--', alpha=0.3)
plt.tight_layout()
#jtplot.style(theme='grade3', fscale=1.2, ticks=True, grid=True)
path_graph = "data/exports/img_regression/2_var_log/"
os.makedirs(path_graph, exist_ok=True)
graph_res_stud('Ukraine')
plt.savefig(path_graph+"2_var_log_6_residus_pays_Ukraine.png", dpi = 200)
plt.show()
graph_res_stud("Afrique du Sud")
plt.savefig(path_graph+"2_var_log_6_residus_pays_Afrique_du_Sud.png", dpi = 200)
plt.show()
graph_res_stud('République Centrafricaine')
plt.savefig(path_graph+"2_var_log_6_residus_pays_Republique_Centraficaine.png", dpi = 200)
plt.show()
La mesure de l'influence d'une observation s'effectue à l'aide de la distance de Cook qui mesure un écart entre $\widehat \beta$ et $\widehat \beta ^{(-i)}$ (calculs effectués avec et sans la i -ème observation)
Méthode pour évaluer l'influence du point i sur la régression:
==> On considère une observation influente si $D_i > \frac {\displaystyle 4}{\displaystyle n-p}$
# creation d'une instance d'Influence
infl = reg_multi_ln.get_influence()
# cacul de la distance de cook de chaque observation
# avec cooks[0] : la distance
# et cooks[1] : p_value associée
cooks = infl.cooks_distance
dis_cooks_l = cooks[0]
# insert de la distance de cook dans le df analyses
analyses['dis_cooks'] = dis_cooks_l
# seuil distance de cook
seuil_cooks = 4/(n-p)
print('Seuil de la distance de Cook : {:.8f}'.format(float(seuil_cooks)))
Seuil de la distance de Cook : 0.00000070
nb=[]
for c in cooks[0]:
if (c - seuil_cooks > 0):
nb.append(c)
nb_influents = len(nb)
part_influents = round(100 * nb_influents / n, 2)
# Nombre de valeurs et pays concernés
df_temp = analyses.loc[analyses['dis_cooks'] > seuil_cooks, ['pays', 'dis_cooks']]
res_influents = df_temp.groupby('pays').size()
nb_pays_avec_influents = len(res_influents)
print(f"Présence de {nb_influents} points influents, soit {part_influents}% du dataset")
print(f"{nb_pays_avec_influents} pays concernés (affichage des 10 pays présentant le plus de valeurs influentes)")
influentes = res_influents.sort_values(ascending=False)
synthese_res['influentes'] = influentes
influentes.head(10)
Présence de 325500 points influents, soit 5.66% du dataset 97 pays concernés (affichage des 10 pays présentant le plus de valeurs influentes)
pays Afrique du Sud 30500 Honduras 22000 Colombie 15500 Bolivie 15500 Brésil 13000 République Centrafricaine 13000 Guatemala 12500 Panama 12000 Chili 10000 États-Unis 10000 dtype: int64
# Représentation graphique des distances de Cook
plt.style.use('seaborn-notebook')
fig = plt.figure(figsize=(10,5))
ax = fig.gca()
ax.yaxis.set_major_formatter(FormatStrFormatter('%.6f'))
values = [1000000*i for i in range(7)]
labels = ["0", "1 Million", "2 Millions", "3 Millions", "4 Millions", "5 Millions", "6 Millions"]
ax.set_xticks(values)
ax.set_xticklabels(labels)
plt.xlabel('Observations')
plt.ylabel('Distance de Cook')
plt.title('Distances de Cook de tous les pays', fontsize=14, color='darkred')
plt.plot(analyses['dis_cooks'])
plt.axhline(y=seuil_cooks, color="red", linestyle="-")
plt.grid(True, linestyle='--', alpha=0.3)
plt.tight_layout()
path_graph = "data/exports/img_regression/2_var_log/"
os.makedirs(path_graph, exist_ok=True)
plt.savefig(path_graph+"2_var_log_7_distance_cooks_glob.png", dpi = 200)
plt.show();
def graph_distance_cook(pays):
df = analyses.loc[analyses['pays'] == pays].reset_index(drop=True)
df = df.drop(columns=['obs']).drop_duplicates().reset_index(drop=True)
df_in = df[df['dis_cooks'] <= seuil_cooks]
df_out = df[df['dis_cooks'] > seuil_cooks]
fig = plt.figure(figsize=(10,5))
ax = fig.gca()
ax.yaxis.set_major_formatter(FormatStrFormatter('%.8f'))
x_in = df_in.index+1
y_in =df_in['dis_cooks']
x_out = df_out.index+1
y_out =df_out['dis_cooks']
plt.bar(x_in, y_in, color="green")
plt.bar(x_out, y_out, color="red")
plt.xlim(-0.05*df.shape[0], 1.05*df.shape[0])
plt.xlabel("observations")
plt.ylabel("distance de Cook")
plt.title("Distance de Cook pour le pays \""+str(pays)+"\"", color="darkred")
milieu_x = df.shape[0] / 2
plt.text(0.8*milieu_x, 1.1 * seuil_cooks, 'Seuil Distance de Cook ({:.8f}'.format(float(seuil_cooks))+')', fontsize = '10', color='teal')
plt.axhline(y=seuil_cooks, color='teal', linestyle='--', zorder=3)
plt.grid(True, linestyle='--', alpha=0.3)
plt.tight_layout()
path_graph = "data/exports/img_regression/2_var_log/"
os.makedirs(path_graph, exist_ok=True)
graph_distance_cook("Chine")
plt.savefig(path_graph+"2_var_log_7_distance_cooks_pays_Chine.png", dpi = 200)
plt.show()
graph_distance_cook("France")
plt.savefig(path_graph+"2_var_log_7_distance_cooks_pays_France.png", dpi = 200)
plt.show()
Création d'un df analyses_res
où ne sont affichés, pour les leviers, les résidus studentisés et les distances de Cook, que les points "atypiques" ou "influents", c'est-à-dire ceux situés dans la région critique
Objectif : comptabiliser le nombre de valeurs atypiques et influentes par pays
analyses_res = analyses.copy()
analyses_res['levier'] = analyses_res['levier'].apply(lambda x: x if x > seuil_levier else np.nan)
analyses_res['res_stud'] = analyses_res['res_stud'].apply(lambda x: x if (x < -seuil_stud or x > seuil_stud) else np.nan)
analyses_res['dis_cooks'] = analyses_res['dis_cooks'].apply(lambda x: x if x > seuil_cooks else np.nan)
analyses_res.columns
Index(['code_pays', 'pays', 'pop', 'gini', 'gdp', 'y_child_avg', 'y_child', 'elas', 'c_i_parent', 'ln_y_child', 'ln_y_child_avg', 'obs', 'levier', 'res_stud', 'dis_cooks'], dtype='object')
analyses_res.sample(10)
code_pays | pays | pop | gini | gdp | y_child_avg | y_child | elas | c_i_parent | ln_y_child | ln_y_child_avg | obs | levier | res_stud | dis_cooks | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1346754 | DNK | Danemark | 5 497 729 | 0.2599 | 34130.0 | 17043.15 | 28955.22 | 0.15 | 60 | 10.273506 | 9.743503 | 1346755 | NaN | NaN | NaN |
3150795 | LUX | Luxembourg | 485 405 | 0.2929 | 73127.0 | 25217.56 | 7315.27 | 0.38 | 29 | 8.897719 | 10.135296 | 3150796 | NaN | NaN | NaN |
1987314 | GRC | Grèce | 11 040 309 | 0.3294 | 27123.0 | 11727.27 | 14271.78 | 0.31 | 70 | 9.566039 | 9.369673 | 1987315 | NaN | NaN | NaN |
115695 | ARM | Arménie | 2 907 618 | 0.2631 | 5611.0 | 1628.38 | 1146.91 | 0.50 | 32 | 7.044827 | 7.395343 | 115696 | NaN | NaN | NaN |
3602013 | MNG | Mongolie | 2 631 898 | 0.3581 | 3286.0 | 2338.09 | 714.10 | 0.40 | 1 | 6.571020 | 7.757089 | 3602014 | NaN | NaN | NaN |
1296055 | DEU | Allemagne | 81 065 752 | 0.3065 | 33758.0 | 18061.72 | 32386.22 | 0.24 | 20 | 10.385488 | 9.801550 | 1296056 | NaN | NaN | NaN |
2656766 | JPN | Japon | 128 538 646 | 0.3211 | 31307.0 | 17432.96 | 7738.26 | 0.34 | 37 | 8.953932 | 9.766118 | 2656767 | NaN | NaN | NaN |
2671569 | JPN | Japon | 128 538 646 | 0.3211 | 31307.0 | 17432.96 | 13795.04 | 0.34 | 15 | 9.532065 | 9.766118 | 2671570 | NaN | NaN | NaN |
4273327 | PER | Pérou | 28 562 317 | 0.4780 | 7858.0 | 3330.53 | 2098.18 | 0.67 | 64 | 7.648825 | 8.110888 | 4273328 | NaN | NaN | NaN |
1571605 | EST | Estonie | 1 340 073 | 0.3007 | 18773.0 | 7702.06 | 6077.21 | 0.40 | 21 | 8.712300 | 8.949243 | 1571606 | NaN | NaN | NaN |
analyses_res2 = analyses_res.groupby('pays')[['levier', 'res_stud', 'dis_cooks']].agg('count')
analyses_res2.sample(3)
levier | res_stud | dis_cooks | |
---|---|---|---|
pays | |||
Cambodge | 0 | 500 | 500 |
Mauritanie | 0 | 2500 | 500 |
Ukraine | 0 | 0 | 0 |
analyses_res2['total'] = analyses_res2.levier + analyses_res2.res_stud + analyses_res2.dis_cooks
analyses_res2 = analyses_res2.sort_values(by="total", ascending=False)
analyses_res2.head(15)
levier | res_stud | dis_cooks | total | |
---|---|---|---|---|
pays | ||||
Afrique du Sud | 50000 | 14500 | 30500 | 95000 |
Honduras | 50000 | 11000 | 22000 | 83000 |
Bolivie | 0 | 10000 | 15500 | 25500 |
Colombie | 0 | 8500 | 15500 | 24000 |
Brésil | 0 | 7500 | 13000 | 20500 |
République Centrafricaine | 0 | 7000 | 13000 | 20000 |
Panama | 0 | 8000 | 12000 | 20000 |
Guatemala | 0 | 7000 | 12500 | 19500 |
Chili | 0 | 5500 | 10000 | 15500 |
États-Unis | 0 | 5000 | 10000 | 15000 |
Paraguay | 0 | 6500 | 8000 | 14500 |
Équateur | 0 | 6500 | 7000 | 13500 |
Mexique | 0 | 6500 | 7000 | 13500 |
République Démocratique du Congo | 0 | 3500 | 8500 | 12000 |
Swaziland | 0 | 4500 | 6500 | 11000 |
########################################################################################################
## Synthèse : les pays avec des outliers ##
########################################################################################################
a = synthese_res['pays_sup_leviers'].index
a_str = ", ".join(a)
print(f" - 2 pays avec des leviers supérieurs au seuil : {a_str}")
b = synthese_res['pays_res_stud'].index
b_top10 = b[:10]
b_str = ", ".join(b_top10)
print(f" - les 10 pays avec le plus de résidus standardisés hors seuil : {b_str}")
c = synthese_res['influentes'].index
c_top10 = c[:10]
c_str = ", ".join(c_top10)
print(f" - les 10 pays avec le plus de points influents : {c_str}")
- 2 pays avec des leviers supérieurs au seuil : Afrique du Sud, Honduras - les 10 pays avec le plus de résidus standardisés hors seuil : Afrique du Sud, Honduras, Bolivie, Colombie, Panama, Brésil, Guatemala, République Centrafricaine, Paraguay, Nicaragua - les 10 pays avec le plus de points influents : Afrique du Sud, Honduras, Colombie, Bolivie, Brésil, République Centrafricaine, Guatemala, Panama, Chili, États-Unis
# on agrège les données par pays
################################
pays_list = list(set(list(a) + list(b) + list(c)))
pays_outliers = analyses_res[analyses_res['pays'].isin(pays_list)]
pays_outliers = pays_outliers.drop(columns=['c_i_parent', 'obs', 'y_child', 'ln_y_child']).reset_index(drop=True)
pays_outliers = pays_outliers.groupby(['code_pays', 'pays', 'pop', 'gini', 'gdp', 'y_child_avg', 'elas', 'ln_y_child_avg'])[['levier', 'res_stud', 'dis_cooks']].count()
pays_outliers['somme_outliers'] = pays_outliers['levier'] + pays_outliers['res_stud'] + pays_outliers['dis_cooks']
pays_outliers = pays_outliers.sort_values(by='somme_outliers', ascending=False)
pays_outliers.head(15)
levier | res_stud | dis_cooks | somme_outliers | ||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
code_pays | pays | pop | gini | gdp | y_child_avg | elas | ln_y_child_avg | ||||
ZAF | Afrique du Sud | 49 779 471 | 0.6698 | 9602.00 | 5617.90 | 0.68 | 8.633714 | 50000 | 14500 | 30500 | 95000 |
HND | Honduras | 7 980 955 | 0.6017 | 3628.00 | 3296.27 | 0.66 | 8.100546 | 50000 | 11000 | 22000 | 83000 |
BOL | Bolivie | 9 721 454 | 0.5615 | 3950.00 | 3016.26 | 0.87 | 8.011774 | 0 | 10000 | 15500 | 25500 |
COL | Colombie | 44 254 975 | 0.5693 | 8185.00 | 3547.01 | 1.10 | 8.173859 | 0 | 8500 | 15500 | 24000 |
BRA | Brésil | 192 030 362 | 0.5445 | 9559.00 | 4807.48 | 0.64 | 8.477929 | 0 | 7500 | 13000 | 20500 |
CAF | République Centrafricaine | 4 273 366 | 0.5617 | 685.00 | 811.30 | 0.66 | 6.698638 | 0 | 7000 | 13000 | 20000 |
PAN | Panama | 3 516 204 | 0.5319 | 11767.00 | 5135.14 | 0.97 | 8.543862 | 0 | 8000 | 12000 | 20000 |
GTM | Guatemala | 14 006 428 | 0.5683 | 4367.00 | 2142.47 | 1.02 | 7.669717 | 0 | 7000 | 12500 | 19500 |
CHL | Chili | 16 708 258 | 0.5316 | 13390.00 | 7051.61 | 0.57 | 8.861011 | 0 | 5500 | 10000 | 15500 |
USA | États-Unis | 303 486 012 | 0.4318 | 43261.00 | 25503.58 | 0.54 | 10.146574 | 0 | 5000 | 10000 | 15000 |
PRY | Paraguay | 6 081 296 | 0.5251 | 4347.00 | 3278.08 | 0.66 | 8.095013 | 0 | 6500 | 8000 | 14500 |
MEX | Mexique | 110 815 271 | 0.5080 | 13434.00 | 3885.83 | 0.66 | 8.265092 | 0 | 6500 | 7000 | 13500 |
ECU | Équateur | 14 535 739 | 0.5099 | 7560.00 | 3383.74 | 1.03 | 8.126737 | 0 | 6500 | 7000 | 13500 |
COD | République Démocratique du Congo | 60 411 195 | 0.4440 | 303.19 | 276.02 | 0.71 | 5.620459 | 0 | 3500 | 8500 | 12000 |
NIC | Nicaragua | 5 667 432 | 0.4828 | 2576.00 | 2519.33 | 0.66 | 7.831750 | 0 | 6500 | 4500 | 11000 |
==> On remarque que, en toute logique, les pays avec le plus d'outliers sont ceux où la répartition des revenus est la plus inégalitaire (cf. un indice de Gini élevé)
On peut également remarquer que seul un pays "riche" figure dans ce résultat, à savoir les Etats-Unis en dixième position (pays avec une répartion très inégale des revenus)
On peut confirmer cette relation en calculant la corrélation entre le nombre d'outliers et l'indice de Gini :
pays_outliers = pays_outliers.reset_index()
cor_gini_outliers = pays_outliers['gini'].corr(pays_outliers['somme_outliers'])
print(f"Coefficient de corrélation entre le nombre de valeurs atypiques et influentes et l'indice de Gini : {cor_gini_outliers:.5f}")
Coefficient de corrélation entre le nombre de valeurs atypiques et influentes et l'indice de Gini : 0.68374
Comment traiter ces différentes valeurs ?
-> cela va dépendre de notre jeu de données et de nos objectifs
Le dataset
Nos objectifs
=> de manière générale, il faut comprendre pourquoi une observation se démarque autant et proposer une solution appropriée :
-> la suppression automatique des observations atypiques et/ou influentes n'est pas LA solution (on peut aussi laisser ces observations, les remplacer par la moyenne ou la médiane....)
Concernant notre dataset :
les valeurs atypiques détectées ne sont pas aberrantes et peuvent être expliquées : -> comme indiqué précédemment, les pays qui ont le plus de valeurs atypiques sont ceux où les revenus sont le moins équitablement répartis : il est donc normal de trouver des valeurs extrêmes dans ces pays
connaissant les pays avec le plus d'outliers et/ou les valeurs les plus élevées, on peut étudier plus précisément ces pays :
Ne dispoant pas d'informations concernant la répartition des clients par pays, nous allons réaliser une correction "automatique" :
-> on supprime les observations qui contiennent des valeurs atypes ET des valeurs influentes.
2 cas différents possibles :
# df des valeurs des outliers
res = analyses_res.copy()
# suppression des observations selon la condition suivante
condition1 = (res['dis_cooks'] > seuil_cooks) & (res['levier'] > seuil_levier)
condition2 = (res['dis_cooks'] > seuil_cooks) & ((res['res_stud'] > seuil_stud) | (res['res_stud'] < -seuil_stud))
condition = condition1 | condition2
# df des valeurs gardées
res_traitement_outliers = res[~condition]
# calcul du nombre de valeurs supprimées
nb_val_suppr = res.shape[0] - res_traitement_outliers.shape[0]
part_val_suppr = round(nb_val_suppr/res.shape[0],2)
print(f"Nombre d'outliers supprimés : {nb_val_suppr}, soit {part_val_suppr}% du dataset")
# calcul du nombre de pays concernés
df_outliers = res[condition]
nb_pays = df_outliers.groupby("pays").size().sort_values(ascending=False).shape[0]
print(f"Nombre de pays concernés : {nb_pays}")
print()
print("Affichage des 10 pays avec le plus de valeurs supprimées")
outliers_pays_gb = df_outliers.groupby("pays").size()
df_pays_outliers = pd.DataFrame(outliers_pays_gb)
df_pays_outliers.columns=['nb_valeurs']
df_pays_outliers['part_en_pct'] = df_pays_outliers['nb_valeurs'].apply(lambda x: '{:.2%}'.format(x/nb_val_suppr))
df_pays_outliers.sort_values(by='nb_valeurs', ascending=False).head(10)
Nombre d'outliers supprimés : 257500, soit 0.04% du dataset Nombre de pays concernés : 97 Affichage des 10 pays avec le plus de valeurs supprimées
nb_valeurs | part_en_pct | |
---|---|---|
pays | ||
Afrique du Sud | 30500 | 11.84% |
Honduras | 22000 | 8.54% |
Bolivie | 10000 | 3.88% |
Colombie | 8500 | 3.30% |
Panama | 8000 | 3.11% |
Brésil | 7500 | 2.91% |
République Centrafricaine | 7000 | 2.72% |
Guatemala | 7000 | 2.72% |
Équateur | 6500 | 2.52% |
Paraguay | 6500 | 2.52% |
# utilisation du module personnel fonction_regression_lineaire disponible dans le dossier "fonctions" du projet
from fonction_regression_lineaire import *
# on lance le modèle avec les nouvelles data
res_model_2v_outliers = ols('ln_y_child ~ ln_y_child_avg+gini', data=res_traitement_outliers).fit()
# on lance la fonction d'analyse de performance du modèle
result_reg = analyse_reg(res_model_2v_outliers, details=None)
Dep. Variable: | ln_y_child | R-squared: | 0.800 |
---|---|---|---|
Model: | OLS | Adj. R-squared: | 0.800 |
Method: | Least Squares | F-statistic: | 1.098e+07 |
Date: | Fri, 18 Jun 2021 | Prob (F-statistic): | 0.00 |
Time: | 20:25:12 | Log-Likelihood: | -4.9484e+06 |
No. Observations: | 5492500 | AIC: | 9.897e+06 |
Df Residuals: | 5492497 | BIC: | 9.897e+06 |
Df Model: | 2 | ||
Covariance Type: | nonrobust |
La p_value est inférieure au seuil de 5% => Le modèle est significatif au niveau global
coef | std err | t | P>|t| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
Intercept | 0.3092 | 0.002 | 124.238 | 0.000 | 0.304 | 0.314 |
ln_y_child_avg | 1.0003 | 0.000 | 4323.928 | 0.000 | 1.000 | 1.001 |
gini | -1.5312 | 0.003 | -489.841 | 0.000 | -1.537 | -1.525 |
Toutes les p_values sont inférieures à 0.05 => toutes les variables sont significatives
df | sum_sq | mean_sq | F | PR(>F) | |
---|---|---|---|---|---|
ln_y_child_avg | 1.0 | 7.704652e+06 | 7.704652e+06 | 2.171078e+07 | 0.0 |
gini | 1.0 | 8.515081e+04 | 8.515081e+04 | 2.399447e+05 | 0.0 |
Residual | 5492497.0 | 1.949160e+06 | 3.548768e-01 | NaN | NaN |
=> En supprimant seulement 0.04% des valeurs initiales, la variance des revenus expliquée par le modèle passe de 73% à 80%.
Mais attention : les suppressions effectuées ici sont automatiques et donc non nécessairement judicieuses :
-> il faudrait sélectionner les suppressions (ou les remplacements des valeurs concernées par la moyenne ou la médiane ou autre) en fonction de données supplémentaires (ex : notre portefeuille client par pays)
X = dcf[['ln_y_child_avg', 'gini']]
X = sm.add_constant(X)
y = dcf['ln_y_child']
model = sm.OLS(y, X)
res = model.fit()
y_np = np.array(y)
y_pred = res.predict(X)
res_pred = pd.DataFrame({'valeurs_reelles': y_np, 'valeurs_predites':y_pred})
res_pred['residus'] = res_pred['valeurs_reelles'] - res_pred['valeurs_predites']
res_pred.sample(5)
res_pred.residus.mean()
valeurs_reelles | valeurs_predites | residus | |
---|---|---|---|
2840666 | 7.833712 | 7.276869 | 0.556843 |
4415605 | 8.722822 | 8.965708 | -0.242887 |
4560344 | 7.276364 | 7.849068 | -0.572704 |
123177 | 7.228235 | 7.327402 | -0.099167 |
1258945 | 9.177410 | 9.629291 | -0.451881 |
2.9582771948417245e-12
def abline(slope, intercept):
"""Plot a line from slope and intercept, borrowed from https://stackoverflow.com/questions/7941226/how-to-add-line-based-on-slope-and-intercept-in-matplotlib"""
axes = plt.gca()
x_vals = np.array(axes.get_xlim())
y_vals = intercept + slope * x_vals
plt.plot(x_vals, y_vals, '--')
plt.plot(y_pred,y_np,'o')
abline(1,0)
plt.xlabel("valeurs prédites")
plt.ylabel("valeurs réelles")
plt.title('Valeurs réelles vs. Valeurs prédites')
plt.grid(True, linestyle='--', alpha=0.3)
path_graph = "data/exports/img_regression/2_var_log/"
os.makedirs(path_graph, exist_ok=True)
plt.savefig(path_graph+"/2_var_log_1_hyp1_linearite.png", dpi = 200)
plt.show();
Différentes méthodes pour vérifier l'absence de colinéarité entre les variables explicatives
La matrice de corrélation
Le Facteur d'Inflation de la Variance (VIF)
## matrice de corrélation
###########################
# calcul des corrélations croisées
mat_corr = dcf[["ln_y_child_avg","gini"]].corr()
# affichage des corrélations croisées
mat_corr
ln_y_child_avg | gini | |
---|---|---|
ln_y_child_avg | 1.000000 | -0.261327 |
gini | -0.261327 | 1.000000 |
# corrélation entre les 2 variables explicatives
mc = dcf['ln_y_child_avg'].corr(dcf['gini'])
print("corr = {:.6f}".format(float(mc)))
# coefficient de détermination de la régression
r2 = reg_multi_ln.rsquared
print("R² = {:.6f}".format(float(r2)))
# corrélation au carré
mc2 = mc**2
print("corr² = {:.6f}".format(float(mc2)))
corr = -0.261327 R² = 0.726129 corr² = 0.068292
# remarque : ici seulement 2 variables explicatives, donc on aurait pu se passer de la matrice et calculer directement la corrélation entre les 2 variables
mc = dcf['ln_y_child_avg'].corr(dcf['gini'])
mc
-0.2613270312072763
## comparaison \corr croisées\ et 0.8
#####################################
if (mc > -0.8) & (mc < 0.8):
print("Absence de colinéarité entre les variables explicatives")
else:
print("Présence de colinéarité entre les variables explicatives")
Absence de colinéarité entre les variables explicatives
## règle de Klein
##################
r2 = reg_multi_ln.rsquared
mc2 = mc**2
if mc2 < r2:
print("Absence de colinéarité entre les variables explicatives")
else:
print("Présence de colinéarité entre les variables explicatives")
Absence de colinéarité entre les variables explicatives
from statsmodels.stats.outliers_influence import variance_inflation_factor
# variables explicatives
X = dcf[["ln_y_child_avg","gini"]]
# ajout de la constante
X = sm.add_constant(X)
# df facteurs VIF
vif_data = pd.DataFrame()
vif_data["feature"] = X.columns[1:]
# calcul du facteur VIF pour chaque variable
vif_data["VIF"] = [variance_inflation_factor(X.values, i) for i in range(1,len(X.columns))]
vif_data
feature | VIF | |
---|---|---|
0 | ln_y_child_avg | 1.073297 |
1 | gini | 1.073297 |
fail = []
strict = []
permissif = []
for i in vif_data.index:
feature = vif_data.iloc[i,0]
vif = vif_data.iloc[i,1]
if vif < 10:
if vif < 5:
strict.append(feature)
else:
permissif.append(feature)
else:
fail.append(feature)
if len(fail) == vif_data.shape[0]:
print("Les facteurs VIF de toutes les variables sont supérieurs à 10 : très forte présomption de colinéarité entre les variables explicatives")
elif len(fail) != 0:
nb_fail = len(fail)
print(f"{nb_fail} variable"+("s" if nb_fail != 1 else "")+" avec un facteur VIF supérieur à 10 : forte présomption de colinéarité entre les variables explicatives")
else:
if len(strict) == vif_data.shape[0]:
print("Les facteurs VIF de toutes les variables sont inférieurs à 5 : donc absence de colinéarité (crière strict)")
else:
print("Les facteurs VIF de toutes les variables sont inférieurs à 10 : donc absence de colinéarité (crière permissif)")
Les facteurs VIF de toutes les variables sont inférieurs à 5 : donc absence de colinéarité (crière strict)
def normalite_residus(model):
display(HTML('<span>## Analyse graphique</span>'))
st.probplot(model.resid, dist="norm", plot= plt)
plt.title('Q-Q plot')
plt.grid(True, linestyle='--', alpha=0.3)
path_graph = "data/exports/img_regression/2_var_log/"
os.makedirs(path_graph, exist_ok=True)
plt.savefig(path_graph+"/2_var_log_2_hyp3_normalite.png", dpi = 200)
plt.show()
display(HTML('<span>## Tests statistiques de normalité</span>'+
'<div class="alert alert-block alert-info" style="width:60%;margin:20px,auto,10px,10px;border-color:gray;">'+
'<ul>'+
'<li style="margin-left:2rem;">H0 : Les résidus suivent une loi normale</li>'+
'<li style="margin-left:2rem;">H1 : Les résidus ne suivent pas une loi normale</li>'+
'<li style="margin-left:2rem;">Seuil de signification : 5%</li></ul></div>'+
'<p style="margin-left:2.5rem;margin-top:2rem;"> Interprétation des tests :</p>'+
'<ul style="margin-left:3.5rem;">'+
'<li>si p_value < 0.05 : on rejette H0 : les variables explicatives ne suivent pas une loi normale</li>'+
'<li>si p_value >= 0.05 : on accepte H0 : les variables explicatives suivent une loi normale</li>'+
'</ul>'))
jb = st.jarque_bera(model.resid)
ks = st.kstest(model.resid, 'norm')
print(f'Jarque-Bera test ---- statistic: {jb[0]:.4f}, p-value: {jb[1]}')
print(f'Kolmogorov-Smirnov test ---- statistic: {ks.statistic:.4f}, p-value: {ks.pvalue:.4f}')
a = jb[1]
b = ks.pvalue
if (a < 0.05) & (b < 0.05):
text_rep = "Les 2 tests conduisent à rejeter HO : on en conclue que les données explicatives ne suivent pas une loi normale"
elif (((a < 0.05) & (b >= 0.05)) | ((a >= 0.05) & (b < 0.05))):
text_rep = "Les 2 tests conduisent à des interprétations différentes : veuillez effectuer d'autres tests pour pouvoir conclure"
else:
text_rep = "Les 2 tests conduisent à accepetr HO : on en conclue que les données explicatives suivent une loi normale"
display(HTML('<p style="font-size:105%;">'+text_rep+'</p>'))
normalite_residus(reg_multi_ln)
Interprétation des tests :
Jarque-Bera test ---- statistic: 1732751.0608, p-value: 0.0 Kolmogorov-Smirnov test ---- statistic: 0.1079, p-value: 0.0000
Les 2 tests conduisent à rejeter HO : on en conclue que les données explicatives ne suivent pas une loi normale
Remarque :
La non normalité de nos variables explicatives est ici à relativiser étant donné la taille de notre échantillon (plus de 5 millions d'onservations).
En effet, le modèle de régression linéaire est un modèle robuste, et ce d'autant plus que la taille de l'échantillon est important :
-> plus l'échantillon est important, plus le modèle est capable de supporter des écarts importants vis à vis de la contrainte de normalité des résidus
X = reg_multi_ln.resid
plt.figure(figsize=(10,5))
sns.displot(x=X, kde=True, palette="Set2", height=6, aspect=2)
plt.title(f'Distribution des residus', color="darkred")
plt.ylabel('Fréquence')
plt.xlabel('Valeur résidus')
plt.grid(True, linestyle='--', alpha=0.3)
plt.tight_layout()
path_graph = "data/exports/img_regression/2_var_log/"
os.makedirs(path_graph, exist_ok=True)
plt.savefig(path_graph+"/2_var_log_3_distribution_des_residus.png", dpi = 200)
plt.show();
<Figure size 1000x500 with 0 Axes>
Hypothèse d'une variance constante des résidus
-> si hétéroscédasticité, il est difficile de déterminer le véritable écart-type des erreurs de prévision => écart-type biaisé
-> or important d'estimer correctement l'écart-type car il est utilisé pour effectuer les tests de signification et pour calculer les intervalles de confiance
-> si écart-type biaisé, risque d'intervalles de confiance trop larges ou trop étroit
Moyens de vérifier le respect de cette hypothèse :
moyen graphique : tracé des résidus par rapport aux valeurs prédites : -> si forme conique du nuage de points => hétéroscédasticité (les résidus augmentent en fonction de la valeur prédite ou du temps)
tests statistiques (test de Bruesch-Pagan, le test de Cook-Weisberg, ou le test général de White par exemple)
from IPython.display import Math
import statsmodels
def cont_var_residus(modele):
display(HTML('<span>## Analyse graphique</span>'))
#ax.scatter(residual, pred_val)
x_min = np.min(y_pred)
x_max = np.max(y_pred)
marge = 0.1 * (x_max - x_min)
x_min -= marge
x_max += marge
plt.figure(figsize=(10,5))
plt.xlim(x_min, x_max)
plt.plot(y_pred,y_np-y_pred,'o', color='CadetBlue', zorder=2)
plt.xlabel(r"Predictions : $\hat{y}$")
plt.ylabel(r"Résidus : y - $\hat{y}$")
plt.title('Prédiction vs Résidus')
plt.tick_params(axis='x')
plt.tick_params(axis='y')
plt.hlines(0, x_min, x_max, color='gray', linestyles='-', zorder=3)
plt.grid(True, linestyle='--', alpha=0.3)
plt.tight_layout()
path_graph = "data/exports/img_regression/2_var_log/"
os.makedirs(path_graph, exist_ok=True)
plt.savefig(path_graph+"/2_var_log_4_predictions_vs_residus.png", dpi = 200)
plt.show()
display(HTML('<span>## Test statistiques d\'homoscédasticité</span>'+
'<div class="alert alert-block alert-info" style="width:60%;margin:20px,auto,10px,10px;border-color:gray;">'+
'<ul">'+
'<li style="margin-left:2rem;">H0 : homoscédasticité des résidus</li>'+
'<li style="margin-left:2rem;">H1 : hétéroscédasticité des résidus</li>'+
'<li style="margin-left:2rem;">Seuil de signification : 5%</li></ul></div>'+
'<p style="margin-left:2.5rem;margin-top:2rem;"> Interprétation des tests :</p>'+
'<ul style="margin-left:3.5rem;">'+
'<li>si p_value < 0.05 : on rejette H0 : on rejette l\'hypothèse d\'homoscédasticité : les variances ne sont pas constantes</li>'+
'<li>si p_value >= 0.05 : on accepte H0 : on accepte l\'hypothèse d\'homoscédasticité : les variances sont constantes</li>'+
'</ul>'))
_, pval, __, f_pval = statsmodels.stats.diagnostic.het_breuschpagan(modele.resid, modele.model.exog)
print(f'Breusch Pagan test ---- p-value: {pval}')
if pval < 0.05:
text_rep = "La p_value est inférieure au seuil de signification de 5 % : on rejette l'hypothèse H0 d'homoscédasticité des résidus : \n => les variances ne sont pas constantes"
else:
text_rep = "La p_value est supérieure ou égale au seuil de signification de 5 % : on accepte l'hypothèse H0 d'homoscédasticité des résidus : \n => les variances dont constantes"
display(HTML('<p style="font-size:105%;">'+text_rep+'</p>'))
cont_var_residus(reg_multi_ln)
Interprétation des tests :
Breusch Pagan test ---- p-value: 0.0
La p_value est inférieure au seuil de signification de 5 % : on rejette l'hypothèse H0 d'homoscédasticité des résidus : => les variances ne sont pas constantes
Le test statistique rejette l'hypothèse d'homoscédasticité des résidus, mais graphiquement, on constate une répartition relativement homogène des résidus le long de l’axe représentant le logatithme des prédictions de revenus.
De plus, tout comme l'hypothèse de normalité des résidus, le modèle de régression linéaire est robuste avec des échantillons de taille importante (il est capable de supporter des écarts importants vis à vis de l'hypothèse de constance de la variance des résidus)
# utilisation du module fonction_regression_lineaire, avec la fonction de verification des hypothèses
y = 'ln_y_child'
X = ['ln_y_child_avg', 'gini']
df = res_traitement_outliers
version_name = "2_var_log_sans_outliers"
verif_hyp_outliers = hypotheses_regression_lineaire(y, X, df, version_name, details=None)
valeurs_reelles | valeurs_predites | residus | |
---|---|---|---|
4823459 | 8.619148 | 8.649182 | -0.030034 |
784702 | 10.212124 | 9.851354 | 0.360770 |
1475193 | 7.358075 | 7.391181 | -0.033106 |
2883494 | 7.329444 | 7.135460 | 0.193984 |
5679851 | 6.806320 | 6.688134 | 0.118186 |
=> si linéarité, alors les points de la droite "prédictions / valeurs réelles" devraient suivre la droite d'équation y=x
pearson | p-value | |
---|---|---|
ln_y_child_avg__gini | -0.286442 | 0.0 |
feature | VIF | |
---|---|---|
0 | ln_y_child_avg | 1.089383 |
1 | gini | 1.089383 |
Interprétation des tests :
Jarque-Bera test ---- statistic: 5050.8948, p-value: 0.0 Kolmogorov-Smirnov test ---- statistic: 0.1253, p-value: 0.0000
Les 2 tests conduisent à rejeter HO : on en conclue que les données ne suivent pas une loi normale
Interprétation des tests :
Breusch Pagan test ---- p-value: 0.0
La p_value est inférieure au seuil de signification de 5% : on rejette l'hypothèse H0 d'homoscédasticité des résidus => les variances ne sont pas constantes
from fonctions_perso import duree_convert
# repère de fin de traitement du notebook
end_time_m4 = time.time()
duree_m4 = end_time_m4 - start_time_m4
duree = duree_convert(duree_m4)
print("Temps total d'exécution du notebook : "+str(duree))
Temps total d'exécution du notebook : 12min 45s