WORLD BANK - PRÉDICTION DE REVENUS

Par Bertrand Delorme, Data Analyst

Retrouvez l'énoncé et les détails de la mission traitée en cliquant ici.

Sommaire

Un code de couleur à été attribué aux différentes missions à titre de repère.




  • .
  • Mission 4: Étude des revenus

    I. Préliminaires des analyses à venir
  • I.1. Récapitulatif et niveau de test
  • I.2. Ajout des variables logarithmiques
  • I.3. Fonctions utilitaires sur les résidus des modélisations
  • I.4. Fonctions utilitaires sur les régression linéaires multiples
  • II. Revenu de l'individu selon le pays d'origine
  • II.1. Aperçu de l'influence du pays d'origine sur le revenu
  • II.2. Modélisation et évaluation du modèle
  • II.3. Test de Fisher
  • II.4. Analyse des résidus
  • II.5. Reprise de l'ANOVA en passant les revenus des individus au logarithme
  • III. Régressions linéaires multiples sur les revenus des individus
  • III.1. Revenu moyen du pays et indice de Gini
  • III.2. Passage au logarithme
  • III.3. Comparaison des deux modèles
  • III.4. Décomposition de la variance expliquée par le second modèle
  • III.5. Ajout de la classe de revenu des parents dans la modélisation
  • III.6. Ajout des variables restantes
  • IV. Bilan sur la variance des revenus
  • IV.1. Explication de la variance des revenus selon nos résultats
  • IV.2. Conclusion
  • IV.3. Bonus: Proposition d'un algorithme de prédiction
  • Nous importons dans un premier temps l'ensemble des librairies nécessaires pour l'ensemble du projet.

    In [1]:
    import pandas as pd  ;  import numpy as np  ;  import random  ;  from IPython.display import Image
    import matplotlib.pyplot as plt  ;  from matplotlib.pylab import arange, plot  ;  from matplotlib.pyplot import figure
    
    from sklearn import preprocessing  ;  from sklearn.preprocessing import StandardScaler ; sc = StandardScaler()
    import scipy  ;  import scipy.stats as st ; from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
    from scipy.stats import t, shapiro, ks_2samp ; from collections import Counter
    import statsmodels.formula.api as smf  ;  import statsmodels.api as sm
    from statsmodels.formula.api import ols  ;  from statsmodels.stats.outliers_influence import variance_inflation_factor
    
    # Ci-dessous sont importées les fonctions données par l'énoncé du projet 7 pour la mission 3, enregistrés dans un dossier .py:
    from codes_mission_3 import* ; from functions import*
    
    # La fonction dim() nous permettra de définir à chaque fois les dimensions des figures que l'on affichera:
    def dim(x, y): return(plt.figure(figsize=(x, y)))
    
    # Outils de génération des cartes du monde colorisées:
    import cartopy ; import cartopy.io.shapereader as shpreader ; import cartopy.crs as ccrs
    shpfilename = shpreader.natural_earth(resolution="110m", category="cultural", name="admin_0_countries")
    reader = shpreader.Reader(shpfilename) ; from world_cartography import *
    
    from IPython.core.display import HTML ; import warnings ; warnings.filterwarnings('ignore')
    HTML("""<style> .output_png {display: table-cell; text-align: center; vertical-align: middle} </style>""")
    
    Out[1]:

    Invoquons notre DataFrame de base - que nous appellerons "df" - issu de la World Income Distribution qui contient les variables suivantes:

    • [country_code]: Le code ISO d'un pays donné.
    • [year_survey]: L'année où ont été relevées les données.
    • [quantile]: Pour chaque pays, on dispose de 100 quantiles, chacun représentant 1% de la population. Ils sont numérotés de 1 à 100 où quantile numéro 1 représente la moyenne des revenus la plus faible et le quantile numéro 100 la moyenne la plus élevée. Cette colonne indique alors le numéro du quantile en question.
    • [nb_quantile]: Renvoie le nombre de quantiles pour un pays donné. En l'occurence, ce nombre est toujours égal à 100.
    • [income]: La moyenne des revenus pour un quantile donné dans un pays donné au cours de l'année en question, converti en dollards.
    • [gdpppp]: Le PIB par habitant à parité de pouvoir d'achats.
    In [2]:
    df=pd.read_csv("data/bank/data_p7.csv").rename(columns={"country": "country_code", "year_survey": "year"}) ; df
    
    Out[2]:
    country_code year quantile nb_quantiles income gdpppp
    0 ALB 2008 1 100 728,89795 7297
    1 ALB 2008 2 100 916,66235 7297
    2 ALB 2008 3 100 1010,916 7297
    3 ALB 2008 4 100 1086,9078 7297
    4 ALB 2008 5 100 1132,6997 7297
    ... ... ... ... ... ... ...
    11594 COD 2008 96 100 810,6233 303,19305
    11595 COD 2008 97 100 911,7834 303,19305
    11596 COD 2008 98 100 1057,8074 303,19305
    11597 COD 2008 99 100 1286,6029 303,19305
    11598 COD 2008 100 100 2243,1226 303,19305

    11599 rows × 6 columns

    Examinons dans un premiers temps le type de chaque colonne de notre base.

    In [3]:
    print(df.info())
    
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 11599 entries, 0 to 11598
    Data columns (total 6 columns):
    country_code    11599 non-null object
    year            11599 non-null int64
    quantile        11599 non-null int64
    nb_quantiles    11599 non-null int64
    income          11599 non-null object
    gdpppp          11399 non-null object
    dtypes: int64(3), object(3)
    memory usage: 543.8+ KB
    None
    

    On constate que les colonnes "income" et "gdpppp" représentent des nombres mais sont interprétés comme des objets. En effet, certaines de ces données sont sous forme de string. Nous convertirons donc ces deux colonnes en nombres flottants, et pour une meilleure visibilité dans toute la suite du sujet, nous arrondirons ces valeurs au cents (centime américain) près.

    In [4]:
    df["income"]=[float(str(i).replace(",",".")) for i in df.income]
    df["gdpppp"]=[float(str(i).replace(",",".")) for i in df.gdpppp]
    df[["income", "gdpppp"]].info()
    
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 11599 entries, 0 to 11598
    Data columns (total 2 columns):
    income    11599 non-null float64
    gdpppp    11399 non-null float64
    dtypes: float64(2)
    memory usage: 181.4 KB
    

    Constatons que le PIB par habitant des Îles Fidji paraissait anormalement élevé par rapport aux autres pays.

    In [5]:
    print("PIB/hbts de la France:", df.loc[df["country_code"]=="FRA", "gdpppp"].unique()[0])
    print("PIB/hbts du Luxembourg:", df.loc[df["country_code"]=="LUX", "gdpppp"].unique()[0])
    print("PIB/hbts des Îles Fidji:", df.loc[df["country_code"]=="FJI", "gdpppp"].unique()[0])
    
    PIB/hbts de la France: 30357.0
    PIB/hbts du Luxembourg: 73127.0
    PIB/hbts des Îles Fidji: 4300332.0
    

    À titre comparatif avec la France et le Luxembourg, tous deux présentants cet indice parmi les plus élevés, notre base de données indique que celui des Îles Fidji serait environ 50 fois plus grand que ces deux derniers. D'après la World Bank - source https://data.worldbank.org/indicator/NY.GDP.PCAP.PP.CD?locations=FJ, cet indice s'élèverait en réalité à environ 7777.7 dollars PPA. Nous le remplacerons donc dans notre base de données.

    In [6]:
    df.loc[df["country_code"]=="FJI", "gdpppp"]=7777.7
    print("PIB/hbts corrigé des Îles Fidji:", df.loc[df["country_code"]=="FJI", "gdpppp"].unique()[0])
    
    PIB/hbts corrigé des Îles Fidji: 7777.7
    

    Intéressons-nous au nombre de pays que contient df et à ses dimensions.

    In [7]:
    print("Nombre de lignes de df: %d" %len(df))
    print("Nombre de pays contenu dans df: %s" %len(df["country_code"].unique()))
    
    Nombre de lignes de df: 11599
    Nombre de pays contenu dans df: 116
    

    Chaque pays est supposé être représenté par 100 quantiles (centiles). Donc les lignes de notre tableau sont censée être exactement au nombre de 100 fois le nombre de pays présents. Les valeurs données ci-dessus nous indiquent donc qu'un centile est absent pour un pays donné. Nous allons donc l'identifier ce pays ainsi que son quantile manquant et ajouter la ligne manquante à df.

    In [8]:
    for cc in df["country_code"].unique(): # Identification du pays concerné
        if len(df.loc[df["country_code"]==cc, "quantile"])<100:
            country=cc
            print("Code ISO du pays concerné:", cc) 
            for i in range(1, 101): # Identification du quantile manquant
                if i not in df.loc[df["country_code"]==country,"quantile"].values: print("Il lui manque le %d ème quantile!" %i)
    
    Code ISO du pays concerné: LTU
    Il lui manque le 41 ème quantile!
    

    Il s'agit donc de la Lituanie, et le 41ème quantile est donc absent. Nous allons donc nous baser sur les revenus moyens des centiles 40 et 42 de la Lituanie puis prendre leur moyenne pour estimer le revenu moyen du centile 41. Puis nous ajoutons cette nouvelle ligne à df.

    In [9]:
    filtre1, filtre2 = (df["country_code"]=="LTU") & (df["quantile"]==40), (df["country_code"]=="LTU") & (df["quantile"]==42)
    rm = (df.loc[filtre1, "income"].values[0] + df.loc[filtre2, "income"].values[0])/2
    df.loc[len(df)]=["LTU", 2008, 41, 100, rm, 17571.0]
    
    # On peut observer que le nombre de lignes de df est bien passé de 11599 à 16000:
    df=df.sort_values(by=["country_code", "quantile"]).reset_index(drop=True) ; df
    
    Out[9]:
    country_code year quantile nb_quantiles income gdpppp
    0 ALB 2008 1 100 728.89795 7297.0
    1 ALB 2008 2 100 916.66235 7297.0
    2 ALB 2008 3 100 1010.91600 7297.0
    3 ALB 2008 4 100 1086.90780 7297.0
    4 ALB 2008 5 100 1132.69970 7297.0
    ... ... ... ... ... ... ...
    11595 ZAF 2008 96 100 24553.56800 9602.0
    11596 ZAF 2008 97 100 28858.03100 9602.0
    11597 ZAF 2008 98 100 35750.29000 9602.0
    11598 ZAF 2008 99 100 46297.31600 9602.0
    11599 ZAF 2008 100 100 82408.55000 9602.0

    11600 rows × 6 columns

    Repérons maintenant les éventuelles colonnes présentant au moins une valeur manquante.

    In [10]:
    df.isnull().sum()
    
    Out[10]:
    country_code      0
    year              0
    quantile          0
    nb_quantiles      0
    income            0
    gdpppp          200
    dtype: int64

    On observe une 200 valeurs manquantes dans la colonne "gdpppp". Repérons alors les pays concernés.

    In [11]:
    [i for i in df.loc[df["gdpppp"].isnull()==True]["country_code"].unique()]
    
    Out[11]:
    ['PSE', 'XKX']

    À titre comparatif avec la France et le Luxembourg, tous deux présentants cet indice parmi les plus élevés, notre base de données indique que celui des Îles Fidji serait environ 50 fois plus grand que ces deux derniers. D'après la World Bank, cet indice s'élèverait en réalité à environ 7777.7 dollars PPA. Nous le remplacerons donc dans notre base de données.

    In [12]:
    df.loc[df["country_code"]=="XKX"]=df.loc[df["country_code"]=="XKX"].fillna(7249)
    df.loc[df["country_code"]=="PSE"]=df.loc[df["country_code"]=="PSE"].fillna(3816.20)
    
    # On peut vérifier que l'on en a terminé avec les valeurs manquantes dans le tableau de base:
    df.isnull().sum()
    
    Out[12]:
    country_code    0
    year            0
    quantile        0
    nb_quantiles    0
    income          0
    gdpppp          0
    dtype: int64

    Récupérons dans un premier temps le nombre d'habitants de chaque pays présents dans notre DataFrame pour l'année en question. Ici, nous avons récupérer sur la faostats la liste des populations de chaque pays de 2004 à 2011 (années où s'étendent nos données de base). Nous appellerons "pop" ce DataFrame. Nous en profiterons pour récupérer également le nom des pays indiqués jusqu'ici par leur code ISO.

    In [13]:
    pop=pd.read_csv("data/bank/gini/population.csv").drop(
        columns=["Code Domaine", "Domaine", "Code Élément", "Élément", "Produit", "Code Produit", "Symbole", 
                 "Description du Symbole", "Code année", "Unité"]).rename(columns={"Code zone": "country_code",
                                                                                   "Zone": "country", "Année": "year", 
                                                                                   "Valeur": "population"})
    pop["population"]=1000*pop.population
    pop.loc[pop["country"]=="Royaume-Uni de Grande-Bretagne et d'Irlande du Nord", "country"]="Royaume-Uni"
    
    df=pd.merge(df, pop, how="left").reindex(columns=["country_code", "country", "year", "quantile", "nb_quantiles", "income",
                                                      "gdpppp", "population"]) ; df.tail(4)
    
    Out[13]:
    country_code country year quantile nb_quantiles income gdpppp population
    11596 ZAF Afrique du Sud 2008 97 100 28858.031 9602.0 50267000.0
    11597 ZAF Afrique du Sud 2008 98 100 35750.290 9602.0 50267000.0
    11598 ZAF Afrique du Sud 2008 99 100 46297.316 9602.0 50267000.0
    11599 ZAF Afrique du Sud 2008 100 100 82408.550 9602.0 50267000.0

    On récupère également les données manquantes à la liste trouvée sur la faostats, à savoir les populations et noms des pays ci-dessous, d'après le site: http://datatopics.worldbank.org/world-development-indicators/.

    In [14]:
    df.loc[df["country_code"]=="BTN", "country"]="Bhoutan"
    df.loc[df["country_code"]=="XKX", "country"]="Kosovo"
    df.loc[df["country_code"]=="SDN", "country"]="Soudan"
    df.loc[df["country_code"]=="SYR", "country"]="Syrie"
    df.loc[df["country_code"]=="PSE", "country"]="Palestine"
    df.loc[df["country_code"]=="COD", "country"]="République du Congo"
    
    df.loc[df["country_code"]=="BTN", "population"]=664876
    df.loc[df["country_code"]=="XKX", "population"]=1747000
    df.loc[df["country_code"]=="SDN", "population"]=33780000
    df.loc[df["country_code"]=="SYR", "population"]=17830000
    df.loc[df["country_code"]=="PSE", "population"]=3689000
    df.loc[df["country_code"]=="COD", "population"]=60410000  ;  df
    
    Out[14]:
    country_code country year quantile nb_quantiles income gdpppp population
    0 ALB Albanie 2008 1 100 728.89795 7297.0 3157000.0
    1 ALB Albanie 2008 2 100 916.66235 7297.0 3157000.0
    2 ALB Albanie 2008 3 100 1010.91600 7297.0 3157000.0
    3 ALB Albanie 2008 4 100 1086.90780 7297.0 3157000.0
    4 ALB Albanie 2008 5 100 1132.69970 7297.0 3157000.0
    ... ... ... ... ... ... ... ... ...
    11595 ZAF Afrique du Sud 2008 96 100 24553.56800 9602.0 50267000.0
    11596 ZAF Afrique du Sud 2008 97 100 28858.03100 9602.0 50267000.0
    11597 ZAF Afrique du Sud 2008 98 100 35750.29000 9602.0 50267000.0
    11598 ZAF Afrique du Sud 2008 99 100 46297.31600 9602.0 50267000.0
    11599 ZAF Afrique du Sud 2008 100 100 82408.55000 9602.0 50267000.0

    11600 rows × 8 columns

    Nous allons rapportés la population de chaque pays présents dans la base à 100 habitants, chacun représenté par un centile. Et nous considérerons le revenu moyen attribué à un centile dans la colonne "income" comme le revenu de ce centile pour calculer l'indice de Gini de chaque pays présents dans "df", puis nous ajouterons ces indices à celui-ci.

    In [15]:
    # On crée ici la liste P de chaque pays présent dans notre base:
    P = df.country_code.unique()
    
    # On crée maintenant la liste G qui contiendra l'indice de Gini de chaque pays présent dans P:
    G=[]
    for i in P:
        revenus = df.loc[df["country_code"]==i].income.values
        lorenz = np.append([0], np.cumsum(revenus)/sum(revenus))
        gini=2*(0.5-lorenz.sum()/len(revenus))
        G=np.append(G, gini)
        
    # Créons le DataFrame contenant chaque pays présents dans notre base accompagné de son indice de Gini:
    gini_list=pd.DataFrame({"country_code": P, "gini": G})
    
    # Puis on l'ajoute à "df" par une jointure gauche:
    df=pd.merge(df, gini_list, how="left") ; df
    
    Out[15]:
    country_code country year quantile nb_quantiles income gdpppp population gini
    0 ALB Albanie 2008 1 100 728.89795 7297.0 3157000.0 0.294624
    1 ALB Albanie 2008 2 100 916.66235 7297.0 3157000.0 0.294624
    2 ALB Albanie 2008 3 100 1010.91600 7297.0 3157000.0 0.294624
    3 ALB Albanie 2008 4 100 1086.90780 7297.0 3157000.0 0.294624
    4 ALB Albanie 2008 5 100 1132.69970 7297.0 3157000.0 0.294624
    ... ... ... ... ... ... ... ... ... ...
    11595 ZAF Afrique du Sud 2008 96 100 24553.56800 9602.0 50267000.0 0.659779
    11596 ZAF Afrique du Sud 2008 97 100 28858.03100 9602.0 50267000.0 0.659779
    11597 ZAF Afrique du Sud 2008 98 100 35750.29000 9602.0 50267000.0 0.659779
    11598 ZAF Afrique du Sud 2008 99 100 46297.31600 9602.0 50267000.0 0.659779
    11599 ZAF Afrique du Sud 2008 100 100 82408.55000 9602.0 50267000.0 0.659779

    11600 rows × 9 columns

    • Nous avons alors une liste de 116 pays dont les données sont relevées chacun pour une seule année donnée entre 2004 et 2011 incluses, exceptée 2005.

    • La colonne "gdpppp" (Gross Domestic Product based on Purchasing Power Parity) représente le PIB à parité de pouvoir d'achat calculé divisé par le nombre d'habitants d'un pays donné.

    • La population de chaque pays est représentée par des centiles, nous avons divisé par 100 chaque population, et regroupé les habitants selon leur revenu. Plus le revenu est élevé, plus l'indice du quantile dans lequel se situe un habitant sera proche de 100, puis nous avons donné le revenu moyen des habitants de chaque quantile dans la colonne "income".

    • Échantillonner une population en utilisant des quantiles est, selon moi, une bonne méthode car cela permet de minimiser le besoin en récoltes des données, et nous permet d'éviter les cas particuliers. Cela nous a permis de donner un indice de Gini à chaque pays dans la colonne "gini" en ne considérants que les revenus moyens des centiles donnés.

    La plupart des données que nous traitons ayant été relevées en 2008. Considérons alors la population mondiale comme la population mondiale de 2008 + les valeurs ajoutées dans la partie précédente par la World Bank, et calculons le pourcentage de la population mondiale couverte par notre analyse.

    In [16]:
    XX=["BTN", "XKX", "SDN", "SYR", "PSE", "COD"]
    eps=df.loc[df["country_code"].isin(XX)==True, "population"].unique().sum()
    
    per=round(100*sum(df.drop_duplicates(["country_code"]).population)/(sum(pop.loc[pop["year"]==2008].population)+eps), 2)
    CARTO="L'analyse couvre donc environ {}% de la population mondiale et les pays ci-dessous".format(per)
    
    ciso=mapzone(df[["country_code", "country"]].drop_duplicates().set_index("country"))
    dim(22, 7); ax = plt.axes(projection=ccrs.PlateCarree()) ; ax = carte(ax) ; cartography([ciso], ["crimson"], ax)
    plt.title(CARTO, fontsize=24) ; plt.show()
    

    Il nous faut sélectionner entre 5 à 10 pays de notre base de données pour montrer la diversité mondiale en terme de distribution de revenus. Pour les sélectionner de manière la plus objective possible, une classification hiérarchique ascendante non-supervisée de ces pays sur df selon les variables "income" et "gdpppp", en tracerons un dendrogramme pour déterminer le nombre de groupes distincts raisonnables à étudier, et sélectionnerons un pays donné par groupe obtenu.

    In [17]:
    var=df[["country_code", "income", "gdpppp"]]
    var=pd.DataFrame(var.groupby("country_code").mean())
    
    X=var.values
    std_scaler = preprocessing.StandardScaler().fit(X)
    X_scaled = std_scaler.transform(X)
    
    df_scaled=pd.DataFrame(X_scaled, index=var.index, columns=var.columns) ; df_scaled
    
    Out[17]:
    income gdpppp
    country_code
    ALB -0.463541 -0.393032
    ARG -0.033358 0.059328
    ARM -0.669573 -0.521797
    AUT 1.593512 1.813852
    AZE -0.559648 -0.331628
    ... ... ...
    VEN -0.437559 -0.052483
    VNM -0.707360 -0.753437
    XKX -0.586963 -0.396698
    YEM -0.757892 -0.780474
    ZAF -0.068034 -0.216991

    116 rows × 2 columns

    In [18]:
    # Préparation des données pour le clustering:
    names = df_scaled.index
    
    # Clustering hiérarchique:
    Z = linkage(X, 'ward')
    plot_dendrogram(Z, names) ; plt.show()
    

    En analysant le dendrogramme, il nous laisse suggérer que réaliser 8 clusters nous permettrait une bonne répartition des diversités.

    In [19]:
    Y = df_scaled.values
    Z = linkage(Y, 'ward')
    
    # On ajoute la colonne "cluster" à "df" qui indique le cluster attribué à un pays pays donné selon notre classification:
    clusters = fcluster(Z, 8, criterion='maxclust')
    var["cluster"]=clusters
    varc=var[["cluster"]].reset_index()
    df=pd.merge(df, varc, how="left") ; df
    
    Out[19]:
    country_code country year quantile nb_quantiles income gdpppp population gini cluster
    0 ALB Albanie 2008 1 100 728.89795 7297.0 3157000.0 0.294624 7
    1 ALB Albanie 2008 2 100 916.66235 7297.0 3157000.0 0.294624 7
    2 ALB Albanie 2008 3 100 1010.91600 7297.0 3157000.0 0.294624 7
    3 ALB Albanie 2008 4 100 1086.90780 7297.0 3157000.0 0.294624 7
    4 ALB Albanie 2008 5 100 1132.69970 7297.0 3157000.0 0.294624 7
    ... ... ... ... ... ... ... ... ... ... ...
    11595 ZAF Afrique du Sud 2008 96 100 24553.56800 9602.0 50267000.0 0.659779 8
    11596 ZAF Afrique du Sud 2008 97 100 28858.03100 9602.0 50267000.0 0.659779 8
    11597 ZAF Afrique du Sud 2008 98 100 35750.29000 9602.0 50267000.0 0.659779 8
    11598 ZAF Afrique du Sud 2008 99 100 46297.31600 9602.0 50267000.0 0.659779 8
    11599 ZAF Afrique du Sud 2008 100 100 82408.55000 9602.0 50267000.0 0.659779 8

    11600 rows × 10 columns

    Nous sélectionnons alors un pays par cluster. On aura pris soin de choisir pour chaque cluster un pays où les indices de Gini de 2004 à 2011 sont pour le mieux donnés par la Banque Mondiale. Nous allons donc construire un DataFrame - que nous appellerons "selection" - contenant la liste des 8 pays choisis, leur indice de gini pour l'année donnée dans notre base et les numéros de cluster auquel ils ont été attribués lors de la classification hierarchique.

    In [20]:
    # Ci-dessous, la liste S des codes ISO des 8 pays sélectionnés:
    S=["FRA", "ESP", "ISL", "LUX", "ARM", "RUS", "UKR", "ARG"]
    
    P=[] # La liste P nous affichera les noms des pays  en question.
    G=[] # La liste G nous affichera, elle, l'indice de gini attribué à chaque pays.
    C=[] # Enfin, La liste C nous indiquera le cluster auquel appartient chaque pays.
    
    for i in S:
        filtre = df["country_code"]==i
        
        country = df.loc[filtre, "country"].unique()[0]
        clust = df.loc[filtre, "cluster"].unique()[0]
        ind_gini = df.loc[filtre, "gini"].unique()[0]
    
        P=np.append(P, country) 
        C=np.append(C, clust)
        G=np.append(G, ind_gini)
    
    # Résumons alors les pays sélectionnés et leurs données dans un DataFrame:
    selection=pd.DataFrame({"country_code": S, "country": P, "gini": G, "cluster": range(1, 9)}) ; selection
    
    Out[20]:
    country_code country gini cluster
    0 FRA France 0.319096 1
    1 ESP Espagne 0.297419 2
    2 ISL Islande 0.275067 3
    3 LUX Luxembourg 0.282935 4
    4 ARM Arménie 0.253108 5
    5 RUS Fédération de Russie 0.406691 6
    6 UKR Ukraine 0.245140 7
    7 ARG Argentine 0.439902 8

    À présent, nous allons représenter graphiquement le revenu moyen en fonction de chacune des classes de revenus pour les 8 pays sélectionnés ci-dessus pour montrer la diversité des cas.

    In [21]:
    figure(num=None, figsize=(8, 6), dpi=80, facecolor="w", edgecolor="k")
    
    for n, i in enumerate(selection["country_code"]):
        filtre = df["country_code"]==i
        plt.plot(df.loc[filtre, "quantile"], df.loc[filtre, "income"], label="Cluster %d" %(n+1))
        
    plt.title("Diversité des classes de revenus dans le monde", fontsize=16) ; plt.legend(fontsize=10)
    plt.xlabel("Quantiles", fontsize=14); plt.ylabel("Revenus Moyens",fontsize=14); plt.yscale("symlog",linthreshx=1); plt.show()
    

    On constate tout d'abord que les étendues des revenus pour chaque pays sont relativement disjointes. Par exemple, le plus haut revenu moyen du cluster 5 est inférieur au plus bas revenu du cluster 4. Cependant, les courbes semblent relativement superposables. On peut donc estimer que les inégalités de revenus entre les différents pays sont plus marquées au niveau international qu'au niveau national.

    Nous allons donc afficher la courbe de Lorenz pour chaque pays sélectionné. Nous indiquerons chaque cluster dans le titre de chaque graphique.

    In [22]:
    figure(num=None, figsize=(8, 5), dpi=80, facecolor="w", edgecolor="k")
    
    for p,i in enumerate(selection["country"]):
        
        data=df.loc[df["country"]==i]
        revenus = data.income.values
        n = len(revenus)
        
        lorenz = np.append([0], np.cumsum(revenus)/sum(revenus))
        gini=selection["gini"][p]
        
        plt.axes().axis("equal")
        plt.plot(np.linspace(0-1/n, 1+1/n, n+1), 
                 lorenz, label=" %s - Cluster %d - Indice de Gini: %f" % (i, p+1, gini))
        X = arange(0,1,0.01) ; Y=X
        plot(X, Y)
    
    plt.title("Courbes de Lorenz", fontsize=20) ; plt.legend(fontsize=9.5) ; plt.tight_layout(); plt.show()
    

    Les indices de Gini calculés se situent entre 0,24 et 0,44. On constatera tout de même de bonnes inégalités en terme de distribution de revenus dans chaque pays. Tous éloignés de 0, on en déduit que les revenus ne sont pas équitablement répartis dans aucun état.

    Le DataFrame "gin" nous indique les indices de gini que la Banque Mondiale a pu estimer pour chaque pays au fil des ans. Nous ne conserverons que la période donnée dans notre base, à savoir les années 2004 à 2011, et uniquement les pays présents dans notre sélection.

    In [23]:
    gin=pd.read_csv("data/bank/gini/api.csv", skiprows=[0,1,2])
    
    # On supprime les colonnes inutiles et on renomme les deux premières:
    gin=gin.drop(columns="Unnamed: 64").rename(columns={"Country Code": "country_code"}).set_index("country_code")
    
    # On ne garde que les années 2004 à 2011:
    gin=gin.iloc[:, 47:]
    gin=gin.iloc[:, 0:8]
    gin=gin.reset_index()
    
    # On ne conserve que les pays présents dans notre sélection:
    gin=gin.loc[gin["country_code"].isin(selection["country_code"])==True]
    
    # On le réordonne comme le DataFrame "selection":
    sel=selection[["country_code"]].reset_index()
    gin=pd.merge(gin, sel).sort_values(by="index").set_index("index").reset_index().drop(columns=["index"])
    
    # Le DataFrame renvoie les indices de Gini en pourcentage, nous les divisons donc par 100:
    gin.iloc[:, 1:]=gin.iloc[:, 1:]/100 ; gin
    
    Out[23]:
    country_code 2004 2005 2006 2007 2008 2009 2010 2011
    0 FRA 0.306 0.298 0.297 0.324 0.330 0.327 0.337 0.333
    1 ESP 0.333 0.324 0.335 0.341 0.342 0.349 0.352 0.357
    2 ISL 0.280 0.290 0.302 0.295 0.318 0.287 0.262 0.268
    3 LUX 0.302 0.308 0.309 0.311 0.326 0.312 0.305 0.321
    4 ARM 0.375 0.360 0.297 0.312 0.292 0.280 0.300 0.294
    5 RUS 0.403 0.413 0.410 0.423 0.416 0.398 0.395 0.397
    6 UKR 0.289 0.290 0.298 0.270 0.266 0.253 0.248 0.246
    7 ARG 0.486 0.480 0.467 0.466 0.453 0.441 0.445 0.427

    Nous allons donc maintenant représenter graphiquement pour ces 8 pays l'évolution de l'indice de Gini de 2004 à 2011.

    In [24]:
    figure(num=None, figsize=(10, 5), dpi=80, facecolor="w", edgecolor="k")
    
    for i in gin.index:
        
        ans=pd.DataFrame(gin.iloc[i, 1:9]).reset_index()
        ans.columns=["year", "gini"]
        
        pays=selection["country"][i]
        groupe=selection["cluster"][i]
        
        plt.plot(ans.year, ans.gini, label=("%s - Cluster %d" %(pays, groupe)))
        
    plt.legend(loc="upper left", fontsize=12, bbox_to_anchor=(1, 1)) ; plt.ylabel("Indice de Gini", fontsize=20)
    plt.title("Évolution de l'indice de Gini au fil des ans", fontsize=25) ; plt.show()
    

    On notera généralement une tendance à la baisse des indices de gini proches de 0,5 au fil des ans, tandis que les indices de gini les plus bas ont présentent une légère tendance à la hausse ou bien semblent osciller autour d'une certaine constante.

    Pour classer les pays choisis selon leur indice de Gini, il nous suffit de reprendre le DataFrame "selection" et de l'ordonner selon la variable "gini". On crée un DataFrame qui renvoie cette liste ordonnée que nous appellerons "classement".

    In [25]:
    classement=selection[["country", "gini", "cluster"]].sort_values(by="gini").reset_index().drop(columns="index") ; classement
    
    Out[25]:
    country gini cluster
    0 Ukraine 0.245140 7
    1 Arménie 0.253108 5
    2 Islande 0.275067 3
    3 Luxembourg 0.282935 4
    4 Espagne 0.297419 2
    5 France 0.319096 1
    6 Fédération de Russie 0.406691 6
    7 Argentine 0.439902 8

    Nous allons maintenant calculer la moyenne de ces indices de gini, ainsi que la moyenne des 4 pays présentant l'indice le plus bas, puis la moyenne des 4 présentant les plus haut.

    In [26]:
    low_classement , top_classement = classement.head(4) , classement.tail(4)
    print("Moyenne des 8 pays:", classement["gini"].mean())
    print("Moyenne des 4 pays présentant les indices de gini les plus bas:", low_classement["gini"].mean())
    print("Moyenne des 4 pays présentant les indices de gini le plus hauts:", top_classement["gini"].mean())
    
    Moyenne des 8 pays: 0.31491964062990735
    Moyenne des 4 pays présentant les indices de gini les plus bas: 0.2640623339174357
    Moyenne des 4 pays présentant les indices de gini le plus hauts: 0.365776947342379
    

    La France semble présenter une distribution de revenus légèrement moins équitable que la plupart des autres pays du monde, mais son indice de gini n'est pas très éloigné de la moyenne générale.

    L'objectif de cette première partie de la mission 3 est de récupérer les coefficients d'élasticité propres à chaque pays présents dans notre base de données et de les ajouter à df. Le DataFrame suivant - qu'on appellera "gdi" - issu de la World Bank, nous indiquera ce coefficient pour une première partie de nos pays.

    In [27]:
    gdi=pd.read_csv("data/bank/gdim.csv")[["wbcode", "year", "IGEincome"]].rename(columns={"wbcode": "country_code", 
                                                                                           "IGEincome": "elasticity"})
    gdi=gdi.sort_values(by=["country_code", "year"]) ; gdi.sort_values(by="elasticity").drop_duplicates()
    
    Out[27]:
    country_code year elasticity
    1837 FIN 1960 0.112876
    1472 DNK 1960 0.145146
    5951 TWN 1960 0.181000
    362 BEL 1960 0.183176
    4527 NOR 1960 0.198667
    ... ... ... ...
    6462 ZAR 1940 NaN
    6468 ZAR 1950 NaN
    6474 ZAR 1960 NaN
    6486 ZAR 1980 NaN
    6492 ZMB 1980 NaN

    589 rows × 3 columns

    Nous allons nous débarrasser des doublons de ce DataFrame, et conserver l'élasticité la plus récente pour les pays où elle est indiquée. Puis nous ajouterons une nouvelle colonne "elasticity" à df qui nous renverra déjà l'élasticité des pays où elle est indiquée dans gdi, et renverra un NaN pour ceux où elle ne l'est pas pour le moment.

    In [57]:
    # Cration de la table "ela" qui représentera "gdi" sans les doublons avec, si elle est indiquée, l'élasticité la plus récente
    # pour un pays donné:
    
    ela=pd.DataFrame({"country_code": [], "year": [], "elasticity": []})
    
    for i in gdi["country_code"].unique():
        loca=gdi["country_code"]==i
        if len(gdi.loc[loca].dropna())>0:
            ok=gdi.loc[loca].dropna().drop_duplicates()
            ela=pd.concat([ela, ok])
        else:
            ko=gdi.loc[loca].drop_duplicates(subset=["elasticity"])
            ela=pd.concat([ela, ko])            
    ela=ela.drop(columns="year")
    
    # Ajout de la colonne "elasticity" à df:
    df=pd.merge(df, ela, how="left")  ;   df
    
    Out[57]:
    country_code country year quantile nb_quantiles income gdpppp population gini cluster elasticity
    0 ALB Albanie 2008 1 100 728.89795 7297.0 3157000.0 0.294624 7 0.815874
    1 ALB Albanie 2008 2 100 916.66235 7297.0 3157000.0 0.294624 7 0.815874
    2 ALB Albanie 2008 3 100 1010.91600 7297.0 3157000.0 0.294624 7 0.815874
    3 ALB Albanie 2008 4 100 1086.90780 7297.0 3157000.0 0.294624 7 0.815874
    4 ALB Albanie 2008 5 100 1132.69970 7297.0 3157000.0 0.294624 7 0.815874
    ... ... ... ... ... ... ... ... ... ... ... ...
    11595 ZAF Afrique du Sud 2008 96 100 24553.56800 9602.0 50267000.0 0.659779 8 0.677000
    11596 ZAF Afrique du Sud 2008 97 100 28858.03100 9602.0 50267000.0 0.659779 8 0.677000
    11597 ZAF Afrique du Sud 2008 98 100 35750.29000 9602.0 50267000.0 0.659779 8 0.677000
    11598 ZAF Afrique du Sud 2008 99 100 46297.31600 9602.0 50267000.0 0.659779 8 0.677000
    11599 ZAF Afrique du Sud 2008 100 100 82408.55000 9602.0 50267000.0 0.659779 8 0.677000

    11600 rows × 11 columns

    On observe que nous n'avons pas encore l'ensemble des indices d'élasticité de tous les pays observés, ce qui fera l'objet de la sous-partie suivante.

    In [58]:
    df["elasticity"].isnull().sum()
    
    Out[58]:
    5300

    Nous allons donc maintenant repérer les pays de notre base de données pour lesquels l'élasticité n'a pas été relevée par gdi. Le DataFrame suivant, "estim_elasticity", propose une estimation de l'élasticité de chaque pays selon leur région continentale. Nous identifierons alors la liste des pays de df où l'élasticité n'est pas encore indiquée ainsi que la région continentale à laquelle appartient chacun de ces pays, puis nous leur attribuerons l'élasticité moyenne indiquée dans la colonne "Base case" du DataFrame "estim_eslasticity" définit ci-dessous.

    In [59]:
    estim_elasticity=pd.read_csv("data/bank/elasticity.csv").set_index("Unnamed: 0").rename_axis(None) ; estim_elasticity
    
    Out[59]:
    Base case Optimistic (high mobility) Pessimistic (low mobility)
    Nordic European countries and Canada 0.20 0.15 0.3
    Europe (except nordic countries) 0.40 0.30 0.5
    Australia/New Zealand/USA 0.40 0.30 0.5
    Asia 0.50 0.40 0.6
    Latin America/Africa 0.66 0.50 0.9

    Identifions les pays de df dont l'élasticité reste à déterminer.

    In [60]:
    B=[i for i in df.loc[df["elasticity"].isnull()==True]["country"].unique()] ; print(B)
    
    ['Argentine', 'Arménie', 'Azerbaïdjan', 'Burkina Faso', 'Bulgarie', 'Bhoutan', 'République centrafricaine', "Côte d'Ivoire", 'Cameroun', 'République du Congo', 'Costa Rica', 'République dominicaine', 'Estonie', 'Fidji', 'Géorgie', 'Honduras', 'Hongrie', 'Indonésie', "Iran (République islamique d')", 'Iraq', 'Islande', 'Israël', 'Cambodge', 'République démocratique populaire lao', 'Libéria', 'Sri Lanka', 'Lituanie', 'République de Moldova', 'Mexique', 'Monténégro', 'Mozambique', 'Mauritanie', 'Niger', 'Nicaragua', 'Philippines', 'Pologne', 'Paraguay', 'Palestine', 'Roumanie', 'Soudan', 'El Salvador', 'Serbie', 'Eswatini', 'Syrie', 'Thaïlande', 'Tadjikistan', 'Timor-Leste', 'Turquie', 'Ukraine', 'Uruguay', 'Venezuela (République bolivarienne du)', 'Kosovo', 'Yémen']
    

    Créons maintenant de la liste des élasticités attribués dans "estim_elasticity" aux régions territoriales auxquelles appartiennent chacun de ces pays.

    In [61]:
    [a, b, c, d, e] = estim_elasticity["Base case"]
    
    K=[e, d, d, e, b, d, e, e,
       e, d, e, b, c, b, e, b, d,
       d, d, a, d, d, b, d, e,
       d, b, b, e, b, e, e, e, e,
       d, b, e, b, e, e, b, e, d, d,
       d, d, b, b, e, e, d, d, e]
    
    # Ajout des valeurs manquantes dans la colonne "elasticity" de "df":
    CC=[i for i in df.loc[df["elasticity"].isnull()==True]["country_code"].unique()]
    ajout=pd.DataFrame({"country_code": CC, "elasticity": K}) ; ajout.head(10)
    
    Out[61]:
    country_code elasticity
    0 ARG 0.66
    1 ARM 0.50
    2 AZE 0.50
    3 BFA 0.66
    4 BGR 0.40
    5 BTN 0.50
    6 CAF 0.66
    7 CIV 0.66
    8 CMR 0.66
    9 COD 0.50

    Nous pouvons donc enfin ajouter les indices d'élasticités manquants dans df afin de compléter le DataFrame.

    In [62]:
    for i in df.index:
        a=df["country_code"][i]
        if (a in list(ajout["country_code"]))==True:
            df["elasticity"][i]=list(ajout.loc[ajout["country_code"]==a]["elasticity"])[0]    
    df
    
    Out[62]:
    country_code country year quantile nb_quantiles income gdpppp population gini cluster elasticity
    0 ALB Albanie 2008 1 100 728.89795 7297.0 3157000.0 0.294624 7 0.815874
    1 ALB Albanie 2008 2 100 916.66235 7297.0 3157000.0 0.294624 7 0.815874
    2 ALB Albanie 2008 3 100 1010.91600 7297.0 3157000.0 0.294624 7 0.815874
    3 ALB Albanie 2008 4 100 1086.90780 7297.0 3157000.0 0.294624 7 0.815874
    4 ALB Albanie 2008 5 100 1132.69970 7297.0 3157000.0 0.294624 7 0.815874
    ... ... ... ... ... ... ... ... ... ... ... ...
    11595 ZAF Afrique du Sud 2008 96 100 24553.56800 9602.0 50267000.0 0.659779 8 0.677000
    11596 ZAF Afrique du Sud 2008 97 100 28858.03100 9602.0 50267000.0 0.659779 8 0.677000
    11597 ZAF Afrique du Sud 2008 98 100 35750.29000 9602.0 50267000.0 0.659779 8 0.677000
    11598 ZAF Afrique du Sud 2008 99 100 46297.31600 9602.0 50267000.0 0.659779 8 0.677000
    11599 ZAF Afrique du Sud 2008 100 100 82408.55000 9602.0 50267000.0 0.659779 8 0.677000

    11600 rows × 11 columns

    Nous allons générer une population dans le cadre de la construction d'un tableau des distributions conditionnelles. Dans la cellule [1] des importations de librairies ont été, entre autres, importées les fonctions données par l'énoncé d'Openclassrooms dans le cadre de la mission 3. Commençons par générer les revenus d'une population et ceux de leurs parents. Nous prendrons pour cette partie un indice d'élasticité de 0,4.

    In [63]:
    # Nombre de quantiles:
    nb_quantiles = 100
    rg = range(1, nb_quantiles+1)
    
    # Nombre d'individus générés et coefficient d'élasticité choisi:
    n, pj = 1000*nb_quantiles, 0.9
    
    # Génération aléatoire des revenus des enfants et de leurs parents:
    y_child, y_parent = generate_incomes(n, pj)
    y_child, y_parent = pd.Series(y_child), pd.Series(y_parent)
    y_child.head()
    
    Out[63]:
    0    0.516241
    1    2.585843
    2    2.682711
    3    0.157545
    4    5.188952
    dtype: float64

    On détermine maintenant la classes de revenu de chaque enfant et de ses parents.

    In [64]:
    c_i_child = quantiles(y_child, 100) # Classes de revenu des enfants.
    c_i_parent = quantiles(y_parent, 100) # Classes de revenu des parents.
    
    # Aperçut des classes de revenus:
    sample=compute_quantiles(y_child, y_parent, nb_quantiles) ; sample
    
    Out[64]:
    y_child y_parents c_i_child c_i_parent
    0 0.516241 0.266469 32 10
    1 2.585843 1.045406 76 52
    2 2.682711 1.086644 77 54
    3 0.157545 0.584790 9 30
    4 5.188952 10.051918 89 99
    ... ... ... ... ...
    99995 2.804669 0.940590 78 48
    99996 1.079474 0.400802 53 19
    99997 0.073039 0.253534 3 9
    99998 1.725147 0.900493 66 46
    99999 3.484454 1.637972 83 69

    100000 rows × 4 columns

    Établissons maintenant le tableau de distribution pour un indice d'élasticité de 0,4. Le DataFrame suivant comprendra en index la classe de revenu d'un enfant, en colonne la classes de revenu d'un parent et dans chaque cellule, la probabilité qu'un parent appartienne à la classe indiquée en index sachant que son enfant appartiennent à la classe de revenu indiquée en colonne et que l'élasticité est de 0,4.

    In [172]:
    cd = conditional_distributions(sample, nb_quantiles)
    dis_rev = pd.DataFrame(cd, columns=range(1, 101), index=range(1, 101)) ; dis_rev
    
    Out[172]:
    1 2 3 4 5 6 7 8 9 10 ... 91 92 93 94 95 96 97 98 99 100
    1 0.259 0.117 0.087 0.071 0.042 0.050 0.043 0.035 0.033 0.031 ... 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
    2 0.104 0.094 0.072 0.069 0.057 0.046 0.031 0.040 0.038 0.034 ... 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
    3 0.088 0.071 0.077 0.045 0.042 0.046 0.032 0.029 0.033 0.028 ... 0.000 0.000 0.001 0.000 0.000 0.000 0.000 0.000 0.000 0.000
    4 0.056 0.058 0.057 0.044 0.053 0.049 0.053 0.047 0.024 0.033 ... 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
    5 0.050 0.043 0.045 0.035 0.054 0.037 0.035 0.033 0.033 0.034 ... 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.001 0.000 0.000
    ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
    96 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 ... 0.023 0.036 0.043 0.036 0.027 0.047 0.037 0.048 0.052 0.062
    97 0.000 0.000 0.000 0.000 0.000 0.001 0.000 0.000 0.000 0.000 ... 0.031 0.031 0.034 0.033 0.041 0.051 0.060 0.039 0.074 0.068
    98 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 ... 0.024 0.036 0.039 0.034 0.054 0.059 0.057 0.067 0.061 0.072
    99 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 ... 0.041 0.036 0.039 0.043 0.043 0.065 0.062 0.067 0.096 0.117
    100 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 ... 0.030 0.024 0.035 0.047 0.048 0.051 0.072 0.090 0.115 0.243

    100 rows × 100 columns

    À titre d'exemple, calculons la probabilité qu'un parent appartienne à la classe de revenus 47 sachant que son enfant appartient à la classe 50 et que l'indice d'élasticité est de 0,4.

    In [173]:
    print("\nP(c_i_parent = {} | c_i_child = {}, pj = {}) = {}".format(47, 50, 0.4, dis_rev.loc[50, 47]))
    
    P(c_i_parent = 47 | c_i_child = 50, pj = 0.4) = 0.02
    

    Cette seconde sous-partie est une anticipation de la sous-partie suivante qui conclura la mission 3. Nous y présentons notamment la fonction qui nous permettra plus tard de construire les classes de revenu des parents des individus de la World Income Distribution. On généralise la sous-partie précédante en créant la fonction "tableau_distributivite" qui prendra en argument un indice d'élasticité pj, et qui nous renverra le tableau des distributions de revenus selon pj pour une population générée aléatoirement selon une loi normale, et un échantillonnage de 100 quantiles.

    In [65]:
    def tableau_distributivite(pj):
        nb_quantiles=100
        n=500*nb_quantiles
        rg=range(1, nb_quantiles+1)
        
        y_child, y_parent = generate_incomes(n, pj)
        y_child, y_parent = pd.Series(y_child), pd.Series(y_parent)
        
        c_i_child=quantiles(y_child, nb_quantiles)
        c_i_parent=quantiles(y_parent, nb_quantiles)
        
        sample=compute_quantiles(y_child, y_parent, nb_quantiles)
        cd=conditional_distributions(sample, nb_quantiles)
        data=pd.DataFrame(cd, columns=rg, index=rg)
        return(data)
    

    Pour une question pratique, nous utiliserons aussi la fonction "effectif" qui prend une série, et renvoie une nouvelle série qui comprend i fois le nombre k si l'index de k est i.

    In [66]:
    def effectif(S):
        W=[]
        for i in S.index:
            nb=int(S[i])
            W+=[i]*nb
        return(W)
    

    Retirons dans un premier temps les colonnes de df qui ne nous servirons plus jusqu'à la fin du projet, et renommons, conformément à l'énoncé, la colonne "gdpppp" en "mj", que nous considérerons dorénavant comme le revenu moyen du pays, ainsi que la colonne "income" en "y_child", qui représentera maintenant le revenu des individus, et la colonne "quantile" en "c_i_child" qui représentera la classe de revenus de ces individus.

    In [67]:
    df=df.drop(columns=["country", "nb_quantiles", "year", "cluster"]).rename(columns={"gdpppp": "mj", "income": "y_child", 
                                                                                       "quantile": "c_i_child"}) ; df
    
    Out[67]:
    country_code c_i_child y_child mj population gini elasticity
    0 ALB 1 728.89795 7297.0 3157000.0 0.294624 0.815874
    1 ALB 2 916.66235 7297.0 3157000.0 0.294624 0.815874
    2 ALB 3 1010.91600 7297.0 3157000.0 0.294624 0.815874
    3 ALB 4 1086.90780 7297.0 3157000.0 0.294624 0.815874
    4 ALB 5 1132.69970 7297.0 3157000.0 0.294624 0.815874
    ... ... ... ... ... ... ... ...
    11595 ZAF 96 24553.56800 9602.0 50267000.0 0.659779 0.677000
    11596 ZAF 97 28858.03100 9602.0 50267000.0 0.659779 0.677000
    11597 ZAF 98 35750.29000 9602.0 50267000.0 0.659779 0.677000
    11598 ZAF 99 46297.31600 9602.0 50267000.0 0.659779 0.677000
    11599 ZAF 100 82408.55000 9602.0 50267000.0 0.659779 0.677000

    11600 rows × 7 columns

    Reprenons maintenant df, et tirons-en le DataFrame "individus" qui représentera notre échantillon, en multipliant chaque ligne de df par 500.

    In [177]:
    individus=pd.concat([df]*500)
    individus=individus.sort_values(["country_code", "c_i_child"]).reset_index(drop=True)
    individus=individus.reindex(columns=["country_code", "c_i_child", "y_child", "mj", "gini", "elasticity", "population"])
    individus
    
    Out[177]:
    country_code c_i_child y_child mj gini elasticity population
    0 ALB 1 728.89795 7297.0 0.294624 0.815874 3157000.0
    1 ALB 1 728.89795 7297.0 0.294624 0.815874 3157000.0
    2 ALB 1 728.89795 7297.0 0.294624 0.815874 3157000.0
    3 ALB 1 728.89795 7297.0 0.294624 0.815874 3157000.0
    4 ALB 1 728.89795 7297.0 0.294624 0.815874 3157000.0
    ... ... ... ... ... ... ... ...
    5799995 ZAF 100 82408.55000 9602.0 0.659779 0.677000 50267000.0
    5799996 ZAF 100 82408.55000 9602.0 0.659779 0.677000 50267000.0
    5799997 ZAF 100 82408.55000 9602.0 0.659779 0.677000 50267000.0
    5799998 ZAF 100 82408.55000 9602.0 0.659779 0.677000 50267000.0
    5799999 ZAF 100 82408.55000 9602.0 0.659779 0.677000 50267000.0

    5800000 rows × 7 columns

    Conformément à ce qui nous est demandé dans l'énoncé de la mission 3, nous allons ajouter au DataFrame "individus" une nouvelle colonne "c_i_parents" représentant la distribution des classes de revenus des parents, dont le taux d'aparitions d'une même classe de revenu pour un parent dont l'enfant présente une classe donné dans un pays donné (donc pour un coefficient d'élasticité donné) sera proportionnel à la probabilié d'appartenance de cette classe sachant ces derniers paramètres.

    In [178]:
    df.to_csv("data/bank/transferts/df_before.csv", index=None)
    individus.to_csv("data/bank/transferts/individus_before.csv", index=None)
    

    L'exécution de l'instruction pour cet ajout étant trop volumineux pour mon matériel, la partie suivante a donc été réalisée sur Collaboratory. C'est pourquoi ci-dessus ont été enregistrés "df" et "individus", et est joint ci-dessous une capture d'écran du code exécuté sur Colaboratory pour obtenir "individus" doté de sa nouvelle colonne "c_i_parents".

    In [30]:
    # Génération de la colonne "c_i_parents":
    Image(filename='data/bank/algo_capture.png')
    
    Out[30]:

    Ci-dessous, on rappelle alors le DataFrame "individus" avec la modification exécutée sur Collaboratory, à savoir l'ajout de la colonne "c_i_parents" qui renvoie pour chaque quantile d'un pays donné une liste de 500 classes de revenu des parents, conformément aux conditions demandées dans l'énoncé du projet.

    In [68]:
    individus=pd.read_csv("data/bank/transferts/individus_after_m3.csv")
    df=pd.read_csv("data/bank/transferts/df_before.csv")
    
    individus=individus.drop(columns=["mj", "country"]).rename(columns={"gdpppp": "mj"})
    individus=individus.reindex(columns=["country_code", "c_i_child", "y_child", "mj", "gini", "c_i_parents", 
                                         "elasticity", "population"])
    
    # On peut visualiser la nouvelle colonne "c_i_parents" dans le DataFrame "individus":
    individus
    
    Out[68]:
    country_code c_i_child y_child mj gini c_i_parents elasticity population
    0 ALB 1 728.89795 7297.0 0.294624 1 0.815874 3157000.0
    1 ALB 1 728.89795 7297.0 0.294624 1 0.815874 3157000.0
    2 ALB 1 728.89795 7297.0 0.294624 1 0.815874 3157000.0
    3 ALB 1 728.89795 7297.0 0.294624 1 0.815874 3157000.0
    4 ALB 1 728.89795 7297.0 0.294624 1 0.815874 3157000.0
    ... ... ... ... ... ... ... ... ...
    5799995 ZAF 100 82408.55000 9602.0 0.659779 100 0.677000 50267000.0
    5799996 ZAF 100 82408.55000 9602.0 0.659779 100 0.677000 50267000.0
    5799997 ZAF 100 82408.55000 9602.0 0.659779 100 0.677000 50267000.0
    5799998 ZAF 100 82408.55000 9602.0 0.659779 100 0.677000 50267000.0
    5799999 ZAF 100 82408.55000 9602.0 0.659779 100 0.677000 50267000.0

    5800000 rows × 8 columns

    Terminons cette mission 3 en supprimant la colonne ["c_i_child] et en ajoutant la colonne ["y_parents"]: Le revenu des parents basé sur le quantile "c_i_parents" du pays donné dans le tableau de base.

    In [181]:
    # Suppression de la colonne "c_i_child":
    individus=individus.drop(columns="c_i_child")
    
    # Ajout de la colonne "y_parents":
    income_p=df[["country_code", "c_i_child", "y_child"]]
    income_p.columns=["country_code", "c_i_parents", "y_parents"]
    individus=pd.merge(individus, income_p)
    
    
    # Enfin, on réorganise les colonnes de "individus":
    individus=individus.reindex(columns=["country_code", "y_child", "y_parents", "c_i_parents", "mj", "gini", "elasticity",
                                         "population"]) ; individus
    
    Out[181]:
    country_code y_child y_parents c_i_parents mj gini elasticity population
    0 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0
    1 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0
    2 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0
    3 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0
    4 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0
    ... ... ... ... ... ... ... ... ...
    5799995 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0
    5799996 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0
    5799997 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0
    5799998 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0
    5799999 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0

    5800000 rows × 8 columns

    On notera que ce nouveau DataFrame contient finalement donc bien les 3 variables demandées:

    • mj, le revenu moyen du pays j indiqué dans la colonne [mj].
    • Gj, l'indice de Gini du pays j indiqué dans la colonne [gini].
    • c_i_parent, la classe de revenu des parents d'un individu dans la colonne [c_i_parents].

    On rappelle pour la mission 4 que nous avons conservé "df" avant la multiplication par 500 des individus.

    Nous réaliserons chaque test statistique au niveau de 5%. On rappelle dans la cellule suivante notre échantillon "individus" décrit à la fin de la mission précédente, et on en profite pour fixer notre niveau de test nt pour toute modélisation qui s'exécutera dans toute la suite du projet.

    In [182]:
    # Fixation du niveau de test:
    nt=0.05
    
    # Chacun des tests sera basé sur notre nouvelle base de données "individus":
    individus
    
    Out[182]:
    country_code y_child y_parents c_i_parents mj gini elasticity population
    0 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0
    1 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0
    2 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0
    3 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0
    4 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0
    ... ... ... ... ... ... ... ... ...
    5799995 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0
    5799996 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0
    5799997 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0
    5799998 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0
    5799999 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0

    5800000 rows × 8 columns

    Dans le cadre des modélisations à venir, nous ajoutons à "individus" les colonnes "log_y_child", "log_y_parents" et "log_mj", respectivements les données de "y_child", "y_parents" et "mj" exprimées en logarithme. Nous ajoutons également "log_y_child" à df car cela va nous être utile dans la partie suivante.

    In [183]:
    df["log_y_child"]=np.log(df["y_child"])
    individus["log_y_child"]=np.log(individus["y_child"])
    individus["log_mj"]=np.log(individus["mj"])
    individus["log_y_parents"]=np.log(individus["y_parents"])  ;  individus
    
    Out[183]:
    country_code y_child y_parents c_i_parents mj gini elasticity population log_y_child log_mj log_y_parents
    0 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0 6.591534 8.895219 6.591534
    1 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0 6.591534 8.895219 6.591534
    2 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0 6.591534 8.895219 6.591534
    3 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0 6.591534 8.895219 6.591534
    4 ALB 728.89795 728.89795 1 7297.0 0.294624 0.815874 3157000.0 6.591534 8.895219 6.591534
    ... ... ... ... ... ... ... ... ... ... ... ...
    5799995 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0 11.319444 9.169727 11.319444
    5799996 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0 11.319444 9.169727 11.319444
    5799997 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0 11.319444 9.169727 11.319444
    5799998 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0 11.319444 9.169727 11.319444
    5799999 ZAF 82408.55000 82408.55000 100 9602.0 0.659779 0.677000 50267000.0 11.319444 9.169727 11.319444

    5800000 rows × 11 columns

    En vue des analyses répétées que nous exécuterons sur les résidus des modélisations à venir, nous créons ici les fonctions décrites ci-dessous qui prendront toutes en entrée une modélisation donnée pour renvoyer un certain résultat sur ses résidus. À noter que les différents tests d'adéquation à une loi gausienne que nous exécuterons testeront donc l'adéquation d'une certaine variable X à la loi normale de paramètres (mean(X), std(X)).

    • 1°) "residus_henry": qui nous superposera les résidus d'une modélisation donnée avec la droite de Henry.
    • 2°) "nuage_residuel": qui nous renverra le nuage de la variance résiduelle.
    • 3°) "analyse_des_residus": qui nous renverra la droite de Henry ET le nuage résiduel dans un subplots horizontal.
    • 4°) "test_shapiro": qui nous renverra les résultats d'un test de Shapiro sur les résidus (peu probable qu'on s'en serve vu la taille des échantillons) .
    • 5°) "test_residus_KS": qui nous renverra les résultats d'un test de Kolmogorov-Smirnoff sur les résidus.
    • 6°) "test_residus_BP": qui nous renverra les résultats d'un test de Breusch-Pagan sur les résidus en vue de l'analyse de leur homoscédasticité.

    Pour chaque analyse des résidus d'un modèle de régression, nous observerons son éventuelle adéquation à la loi normale sur la droite de Henry, et son homoscédasticité sur le nuage de variance résiduelle.

    In [213]:
    # 1°) Droite de Henry:
    def residus_henry(MODEL, nom, couleur, ax):
        sm.qqplot(MODEL.resid, ax=ax, line="45", fit=True, color=couleur)
        ax.set_title("Droite de Henry - Résidus de %s" %nom, fontsize=18)
        ax.set_xlabel("Quantiles théoriques" ,fontsize=16), ax.set_ylabel("Quantiles observés", fontsize=16)
        
    # 2°) Nuage de la variance résiduelle:
    def nuage_residuel(MODEL, nom, couleur, ax):
        ax=plt.plot(MODEL.fittedvalues, MODEL.resid, ".", color=couleur, alpha=0.3)
        plt.title("Nuage de la variance résiduelle de %s" %nom, fontsize=18)
        plt.xlabel("GWh", fontsize=16), plt.ylabel("Résidus", fontsize=16)
        
    # 3°) Les deux en même temps:
    def analyse_des_residus(MODEL, nom, couleur):
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
        residus_henry(MODEL, nom, couleur, ax1)
        nuage_residuel(MODEL, nom, couleur, ax2)
        
    # 4°) Test de Shapiro:
    def test_shapiro(MODEL):
        SH=shapiro(MODEL.resid)
        print("TEST DE SHAPIRO:", " statistic =", SH[0], " pvalue =", SH[1])
    
    # 5°) Test de Kolmogorov-Smirnov:
    def test_residus_KS(MODEL):
        MR=MODEL.resid
        KS=ks_2samp(MR,list(np.random.normal(MR.mean(), MR.std(), len(MR))))
        print("TEST DE KOLMOGOROV-SMIRNOV:", " statistic =", KS[0], " pvalue =", KS[1])
        
    # 6°) Test de Breusch-Pagan:
    def test_residus_BP(MODEL):
        BP=sm.stats.diagnostic.het_breuschpagan(MODEL.resid, MODEL.model.exog)
        print("TEST DE BREUSCH-PAGAN:", " statistic =", BP[0], " pvalue =", BP[1])
    

    Les analyses de la mission 4 se porteront notamment sur diverses modélisations de données. Pour chaque régression linéaire multiple que nous exécuterons, il nous faudra analyser la multicolinéarité des variables explicatives incluses dans le modèle. La fonction suivante "VIF" nous prendra en argument un modèle de régression linéaire multiple donné et la liste des variables explicatives du dit-modèle pour nous renvoyer un DataFrame indiquant le facteur d'influence de la variance (VIF en anglais) de chacune de ces variables explicatives au cours de la modélisation.

    In [214]:
    def VIF(RLM, variables):
        MEX=RLM.model.exog
        return(pd.DataFrame({"VIF de la variable": [variance_inflation_factor(MEX, i) for i in np.arange(1, MEX.shape[1])]},
                           index=variables))
    

    Nous créons également, ici, la fonction "decomposition_des_variables" qui, pour une régression linéaire multiple donnée, nous renverra directement un DataFrame affichant pour chaque variable explicative le pourcentage d'explication de la variance du revenu des individus.

    In [215]:
    def decomposition_des_variables(RLM):
        data=sm.stats.anova_lm(RLM, typ=1)
        ssq=data[["sum_sq"]]
        ssq=np.round(100*ssq/ssq.sum(), 2)
        ssq["sum_sq"]=ssq.sum_sq
        ssq.columns=["Pourcentage de l'explication de la variance"]
        return(ssq)
    

    Dans cette première partie, on se demande si le pays d'origine a une influence sur le revenu de l'individu. En vue d'une ANOVA sur le revenu en ne considérant que le pays d'origine comme variable explicative, nous allons dans un premier temps rappeler un aperçu de la diversité des revenus dans les pays étudiés. Étant donné que les variables "country_code" et "y_child" de "individus" sont celles de "df" répétées 500 fois, nous utiliserons dans cette première partie df au lieu de "individus" en dépit du poids de ce dernier DataFrame, cela ne changera rien à nos résultats.

    Nous observerons que les boxplots suivantes indiquent déjà qu'il y a bien un effet "pays" sur les revenus.

    In [187]:
    fig, ax = plt.subplots(figsize=(14,7))
    df.boxplot(column="y_child",by="country_code", ax=ax)
    
    plt.title("Revenus par pays", fontsize=28) ; plt.ylabel("Revenus", fontsize=23) ; plt.xlabel("")
    plt.xticks(rotation=90, fontsize=7) ; plt.show()
    

    On modélise alors ici notre ANOVA que nous appellerons "AN". Le DataFrame "individus" étant trop volumineux pour cette exécution, nous la ferons sur df (qui est toujours notre modèle de base avant d'avoir multiplité chaque ligne par 500 au cours de la mission 3), ce qui ne changera rien à nos résultats étant donnée que les salaires sont identiquement répétés 500 fois pour chaque pays dans "individus".

    In [217]:
    AN = ols("y_child ~ country_code", data=df).fit(alpha=nt) #On rappelle que nt=0,05.
    AN.summary().tables[0]
    
    Out[217]:
    OLS Regression Results
    Dep. Variable: y_child R-squared: 0.496
    Model: OLS Adj. R-squared: 0.491
    Method: Least Squares F-statistic: 98.43
    Date: Mon, 18 Jan 2021 Prob (F-statistic): 0.00
    Time: 20:15:46 Log-Likelihood: -1.1862e+05
    No. Observations: 11600 AIC: 2.375e+05
    Df Residuals: 11484 BIC: 2.383e+05
    Df Model: 115
    Covariance Type: nonrobust

    Le coefficient de détermination donné par R-squared dans le modèle indique la performance du modèle. On notera toutefois qu'il diffère, ici, du coefficient de détermination ajusté.

    In [218]:
    print("Coefficient de détermination:", AN.rsquared)
    print("Le pays d'origine explique environ", 100*round(AN.rsquared, 3), "% de  la variance de y_child")
    
    Coefficient de détermination: 0.49639014231099543
    Le pays d'origine explique environ 49.6 % de  la variance de y_child
    

    Nous allons exécuter un test de Fisher sur nos données. On émet alors les deux hypothèses suivantes:

    • H0: Le pays d'origine n'a aucune influence sur le revenu de l'individu.
    • H1: Le pays d'origine conditione le revenu de l'individu.

    La validation de H0 se confirmera si les différences entre chaque revenu ne sont pas significatives, à contrario de H1 qui sera validé si elles le sont.

    In [219]:
    sm.stats.anova_lm(AN, typ=1)
    
    Out[219]:
    df sum_sq mean_sq F PR(>F)
    country_code 115.0 5.102375e+11 4.436848e+09 98.429271 0.0
    Residual 11484.0 5.176586e+11 4.507651e+07 NaN NaN

    La pvalue étant infinitésimalement proche de 0, on peut au niveau de test de 5% rejeter H0 et admettre H1: ainsi on en conclu, selon ce test, que le pays d'origine influe bien sur les revenus.

    Distribution des résidus: Nous allons dans un premier temps vérifier l'adéquation de la distribution des résidus de l'ANOVA à la loi normale de paramètres la moyenne et l'écart-type de cette variable, l'hypothèse nulle H0 étant que les résidus ne suivent pas une loi normale, effectuons un test de Kolmogorov-Smirnov sur les résidus.

    In [220]:
    test_residus_KS(AN)
    
    TEST DE KOLMOGOROV-SMIRNOV:  statistic = 0.24767241379310345  pvalue = 7.175258041e-314
    

    Le test de Kolmogorov-Smirnov affichant une pvalue infinitésimalement nulle, il ne permet pas le rejet de l'hypothèse nulle. Observons alors la distribution des résidus sur une droite de Henry.

    In [221]:
    fig, ax = plt.subplots(1, 1, figsize=(6, 5))
    residus_henry(AN, "AN", "darkslategrey", ax) ; plt.show()
    

    À la vue de la droite de Henry, les points ne sont pas alignés et l'on remarque un grand nombre de points éloignés de la moyenne, ce qui laisse fortement supposer que les résidus ne suivent pas une loi normale, pas même si l'on supprimait quelques pays à forte influence de nos observations.

    Homoscédasticité: Nous allons maintenant observer la variance des résidus. Avec comme hypothèse nulle l'hétéroscédasticité des résidus, un test de Breusch-Pagan sur les résidus nous indiquera ou non son rejet.

    In [222]:
    test_residus_BP(AN)
    
    TEST DE BREUSCH-PAGAN:  statistic = 424.6129209876233  pvalue = 4.725291098347324e-37
    

    Le test de Breusch-Pagan ne nous permet pas le rejet de H0. Observons alors les variances des résidus sur le nuage de variance résiduelle.

    In [223]:
    fig, ax = plt.subplots(1, 1, figsize=(6, 5))
    nuage_residuel(AN, "AN", "darkslategrey", ax) ; plt.show()
    

    Le nuage de la variance résiduelle montre clairement que la variance des résidus n'est pas constante et le test de Breusch-Pagan affichant une pvalue proche de 0, on rejette alors l'homoscédasticité des résidus.

    Nous allons ajouter à df la variable "log_y_child" qui sera la variable "y_child" exprimée en logarithme, puis, les résultats de la première ANOVA laissant quelque peu à désirer en dépit de l'analyse des résidus. Nous réaliserons à nouveau chacune des étape précédente pour cette nouvelle ANOVA, à commencer par les boîtes à moustaches.

    In [196]:
    fig, ax = plt.subplots(figsize=(14,7))
    df.boxplot(column="log_y_child",by="country_code", ax=ax)
    
    plt.title("Logarithme des revenus par pays", fontsize=28) ; plt.ylabel("Logarithme des revenus", fontsize=23) ; plt.xlabel("")
    plt.xticks(rotation=90, fontsize=7) ;  plt.show()
    

    Le passage au logarithme permet effectivement de diminuer le volume de grands nombres représentants une série. On observe toujours une irrégularité de la variance des revenus, mais déjà une distribution plus serrée. Avec comme variable explicative "country_code" et comme varialbe à expliquer "log_y_child", on réalise maintenant l'ANOVA dont nous appellerons "AN2" la modélisation.

    In [224]:
    AN2 = ols("log_y_child ~ country_code", data=df).fit(alpha=nt)  ;  AN2.summary().tables[0]
    
    Out[224]:
    OLS Regression Results
    Dep. Variable: log_y_child R-squared: 0.729
    Model: OLS Adj. R-squared: 0.727
    Method: Least Squares F-statistic: 269.0
    Date: Mon, 18 Jan 2021 Prob (F-statistic): 0.00
    Time: 20:16:26 Log-Likelihood: -12627.
    No. Observations: 11600 AIC: 2.549e+04
    Df Residuals: 11484 BIC: 2.634e+04
    Df Model: 115
    Covariance Type: nonrobust

    On relève dans le sommaire de la modélisation le R-squared qui évalue la performance du modèle en nous donnant le pourcentage d'explication de la variance des revenus des individus.

    In [225]:
    print("Coefficient de détermination:", AN2.rsquared)
    print("Le pays d'origine explique environ", round(100*AN2.rsquared, 2), "% de  la variance de log_y_child")
    
    Coefficient de détermination: 0.7292381117731964
    Le pays d'origine explique environ 72.92 % de  la variance de log_y_child
    

    On peut d'ores et déjà admettre que le passage au logarithme des revenus nous permet de modéliser un modèle bien plus performant que le précédent, avec 72,92% d'explication de variance, contre 49,6% pour le précédent. Soulignons l'effet du pays sur les revenus malgré la réduction de leur variance lors du passage au logarithme par le test de Fisher.

    In [226]:
    sm.stats.anova_lm(AN2, typ=1)
    
    Out[226]:
    df sum_sq mean_sq F PR(>F)
    country_code 115.0 16134.417851 140.299286 268.95348 0.0
    Residual 11484.0 5990.615921 0.521649 NaN NaN

    La pvalue étant nulle, à nouveau, nous concluons également à une influence significative du pays d'origine sur les revenus des individus malgré leur variance réduite, ce qui au passage, appui davantage cette même conclusion pour le modèle précédent. Nous allons maintenant analyser les résidus de cette seconde ANOVA, en testant cette fois-ci simultanément leur distribution par un test de Kolmogorov-Smirnov et leur homoscédasticité par un test de Breusch-Pagan.

    In [227]:
    test_residus_KS(AN2) # Test de Kolmogorov-Smirnov:
    test_residus_BP(AN2) # Test de Breusch-Pagan:
    
    TEST DE KOLMOGOROV-SMIRNOV:  statistic = 0.034999999999999976  pvalue = 1.3125734970814965e-06
    TEST DE BREUSCH-PAGAN:  statistic = 787.4802306199363  pvalue = 9.297990032637319e-101
    

    Les pvalues sont effectivement quasi-nulles. On ne peut pour le moment ni rejeter l'idée que la distribution soit gaussienne, ni rejeter son hétéroscédasticité. Néanmoins, observons tout de même encore la droite de Henry et le nuage de la variance résiduelle.

    In [228]:
    analyse_des_residus(AN2, "AN2", "goldenrod") ; plt.show()
    

    On observe donc sur la droite de Henry un bien meilleur alignement des résidus que pour le modèle précédent. En analysant cette droite, les données semblent suivrent une loi normale excepté pour une poignée d'outliers influants sur le modèle. De plus, on observe sur le nuage résiduel une tendance à la constance des variances. Nous étudierons plus tard dans la mission les individus faussant les résultats des tests statistiques précédents, mais nous pouvons, a priori, nous fier à cette nouvelle ANOVA selon les deux graphiques ci-dessus. Ce modèle est clairement plus fiable que le précédent, en plus d'être plus significatif à la vue des pourcentages d'explication de la variance.

    On suppose qu'il existe une relation linéaire entre la variable à expliquer y_child et les variables explicatives mj et gini, donc qu'il existe des paramètres b1, b2 et r tels que: y_child = b1 x mj + b2 x gini + r.

    On émet deux à deux les quatre hypothèses de test:

    • H0: b1=0 - le paramètre de la variable mj n'est pas significatif.
    • H1: b1≠0 - le paramètre de la variable mj est significatif.
    • G0: b2=0 - le paramètre de la variable gini n'est pas significatif.
    • G1: b2≠0 - le paramètre de la variable gini est significatif.

    On effectue maintenant le test de régression linéaire sur y_child selon les variables mj et gini. On appellera "RL1" cette modélisation.

    In [229]:
    RL1=ols("y_child ~ mj+gini", data=individus).fit(alpha=nt)  ;  RL1.summary()
    
    Out[229]:
    OLS Regression Results
    Dep. Variable: y_child R-squared: 0.448
    Model: OLS Adj. R-squared: 0.448
    Method: Least Squares F-statistic: 2.351e+06
    Date: Mon, 18 Jan 2021 Prob (F-statistic): 0.00
    Time: 20:18:04 Log-Likelihood: -5.9578e+07
    No. Observations: 5800000 AIC: 1.192e+08
    Df Residuals: 5799997 BIC: 1.192e+08
    Df Model: 2
    Covariance Type: nonrobust
    coef std err t P>|t| [0.025 0.975]
    Intercept -632.9452 14.798 -42.773 0.000 -661.949 -603.942
    mj 0.4857 0.000 2010.627 0.000 0.485 0.486
    gini 1783.8890 35.384 50.416 0.000 1714.538 1853.240
    Omnibus: 7170721.799 Durbin-Watson: 0.114
    Prob(Omnibus): 0.000 Jarque-Bera (JB): 1903999691.902
    Skew: 6.545 Prob(JB): 0.00
    Kurtosis: 90.791 Cond. No. 2.37e+05


    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
    [2] The condition number is large, 2.37e+05. This might indicate that there are
    strong multicollinearity or other numerical problems.

    Nous obtenons que les 2 pvalues sont de 0 pour chacune des deux variables, ce qui nous conduit à:

    • rejeter H0 et admettre H1.
    • rejeter G0 et admettre G1.

    Les deux variables mj et gini sont alors significatives au niveau de test de 5%. Testons néanmoins leur éventuelle colinéarité.

    In [230]:
    VIF(RL1, ["mj", "gini"])
    
    Out[230]:
    VIF de la variable
    mj 1.185672
    gini 1.185672

    Les deux facteurs d'influence sur la variance sont inférieurs à 10. Il n'y a donc pas de problème de colinéarité dans notre modèle.

    On suppose qu'il existe une relation linéaire entre la variable à expliquer log_y_child et les variables explicatives log_mj et gini, donc qu'il existe des paramètres b1, b2 et r tels que: log_y_child = b1 x log_mj + b2 x gini + r.

    On émet deux à deux les quatre hypothèses de test:

    • H0: b1=0 - le paramètre de la variable log_mj n'est pas significatif.
    • H1: b1≠0 - le paramètre de la variable log_mj est significatif.
    • G0: b2=0 - le paramètre de la variable gini n'est pas significatif.
    • G1: b2≠0 - le paramètre de la variable gini est significatif.

    On effectue maintenant le test de régression linéaire sur log_y_child selon les variables log_mj et gini. On appelle ce second modèle "RL2", et "srl_2" son sommaire.

    In [231]:
    RL2=ols("log_y_child ~ log_mj+gini", data=individus).fit(alpha=nt)  ;  RL2.summary()
    
    Out[231]:
    OLS Regression Results
    Dep. Variable: log_y_child R-squared: 0.652
    Model: OLS Adj. R-squared: 0.652
    Method: Least Squares F-statistic: 5.429e+06
    Date: Mon, 18 Jan 2021 Prob (F-statistic): 0.00
    Time: 20:18:32 Log-Likelihood: -7.0428e+06
    No. Observations: 5800000 AIC: 1.409e+07
    Df Residuals: 5799997 BIC: 1.409e+07
    Df Model: 2
    Covariance Type: nonrobust
    coef std err t P>|t| [0.025 0.975]
    Intercept 0.7721 0.003 234.835 0.000 0.766 0.779
    log_mj 0.8656 0.000 3016.633 0.000 0.865 0.866
    gini -1.4920 0.004 -376.609 0.000 -1.500 -1.484
    Omnibus: 196030.739 Durbin-Watson: 0.048
    Prob(Omnibus): 0.000 Jarque-Bera (JB): 531495.049
    Skew: -0.102 Prob(JB): 0.00
    Kurtosis: 4.469 Cond. No. 125.


    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

    Nous obtenons que les 2 pvalues sont de 0 pour chacune des deux variables, donc b1 et b2 sont significativement différents de 0, ce qui nous conduit à:

    • rejeter H0 et admettre H1.
    • rejeter G0 et admettre G1.

    Les deux variables log_mj et gini sont alors significatives au niveau de test de 5%. Testons également leur colinéarité.

    In [232]:
    VIF(RL2, ["log_mj", "gini"])
    
    Out[232]:
    VIF de la variable
    log_mj 1.095369
    gini 1.095369

    Les deux facteurs d'influence sur la variance sont inférieurs à 10. Il n'y a donc pas de problème de colinéarité dans notre modèle non plus.

    Nous allons analyser le pourcentage de la variance expliquée des revenus par chacun des deux derniers modèles de régressions linéraires RL1 où les données sont inchangées et RL2 où les données sont exprimées en logarithme. Les droites de Henry sur les résidus de chacune des deux régressions linéaires précédentes nous indiqueront lesquels sont le plus en adéquation avec la loi gaussienne.

    In [233]:
    # Analyse de la première régression linéaire sans passer au logarithme:
    print("COEFFICIENT DE DÉTERMINATION DE RL1 - SANS PASSER AU LOGARITHME:", round(RL1.rsquared, 4))
    analyse_des_residus(RL1, "RL1", "purple") ; plt.show()
    
    print("_"*60) ; print(" ")
    
    # Analyse de la seconde régression linéaire en passant au logarithme:
    print("COEFFICIENT DE DÉTERMINATION DE RL2 - EN PASSANT AU LOGARITHME:", round(RL2.rsquared, 4))
    analyse_des_residus(RL2, "RL2", "mediumblue") ; plt.show()
    
    COEFFICIENT DE DÉTERMINATION DE RL1 - SANS PASSER AU LOGARITHME: 0.4477
    
    ____________________________________________________________
     
    COEFFICIENT DE DÉTERMINATION DE RL2 - EN PASSANT AU LOGARITHME: 0.6518
    

    Le pourcentage de la variance expliquée par le premier modèle est d'environ 44,77% tandis que celui du second modèle est proche de 65,18%, donc un coefficient de détermination plus élevé pour RL2 que pour RL1.

    Les droites de Henry indiquent clairement que les résidus du second modèle (en passant au logarithme) sont bien plus en adéquation avec une loi gaussienne que ne le sont les résidus du premier modèle:

    • Pour le premier modèle, sans passer au logarithme, nous observons un éparpillement total des résidus, comme pour la dernière modélisation, aucunement une adéquation à la loi gaussienne, et nul échantillonnage raisonnable de nos individus ne semblerait y remédier.
    • Pour la deuxième régression linéaire, en revanche, où l'on passe au logarithme, la distribution empirique des résidus semble suivre une loi gaussienne selon la droite de Henry, excepté pour les valeurs faibles des résidus.

    Les nuages de variance résiduelle indiquent clairement que les résidus du second modèle (en passant au logarithme) sont bien plus proches:

    • Pour le premier modèle, sans passer au logarithme, les variances diffèrent significativement.
    • Pour la deuxième régression linéaire, en revanche, où l'on passe au logarithme, les variances semblent toutes plus ou moins égales entre elles.

    C'est donc indégnablement ce second modèle que nous considérerons pour la suite, le premier n'étant pas viable. Le passage au logarithme nous a donc bien permis un modèle de régression linéaire plus fiable que le premier réalisé.

    On s'intéresse maintenant au pourcentage de la variance de "log_y_child" expliquée par chacune des deux variables "log_mj" et "gini". Pour les obtenir, nous modéliserons la table de la régression linéaire de RL2 que nous appellerons "table_RL2". Nous appliquons ici sur RL2 la fonction "decomposition_des_variables" définie dans la partie I de la mission 4 pour observer le pourcentage de l'explication de la variance de "log_y_child" de chaque variable du modèle.

    In [234]:
    table_RL2=sm.stats.anova_lm(RL2, typ=2)
    decomposition_des_variables(RL2)
    
    Out[234]:
    Pourcentage de l'explication de la variance
    log_mj 64.33
    gini 0.85
    Residual 34.82

    Ainsi, log_mj explique 64,33% de la variance de log_y_child tandis que la variable gini n'apporte qu'une contribution de 0,85%. On observe sur les résidus que cette variance est expliquée à 34,82% par d'autres facteurs.

    Notre dernier modèle de régression linéaire explique environ 65,18% de la variance. Voyons si la variable "c_i_parents" contribue à nous l'expliquer davantage. On modélise donc une nouvelle régression linéaire "RL3", qui reprend "RL2" en y ajoutant les classes de revenu des parents des individus.

    In [235]:
    RL3=ols("log_y_child ~ log_mj+gini+c_i_parents", data=individus).fit(alpha=nt) ; RL3.summary()
    
    Out[235]:
    OLS Regression Results
    Dep. Variable: log_y_child R-squared: 0.703
    Model: OLS Adj. R-squared: 0.703
    Method: Least Squares F-statistic: 4.577e+06
    Date: Mon, 18 Jan 2021 Prob (F-statistic): 0.00
    Time: 20:25:50 Log-Likelihood: -6.5813e+06
    No. Observations: 5800000 AIC: 1.316e+07
    Df Residuals: 5799996 BIC: 1.316e+07
    Df Model: 3
    Covariance Type: nonrobust
    coef std err t P>|t| [0.025 0.975]
    Intercept 0.2248 0.003 72.873 0.000 0.219 0.231
    log_mj 0.8656 0.000 3266.548 0.000 0.865 0.866
    gini -1.4919 0.004 -407.785 0.000 -1.499 -1.485
    c_i_parents 0.0108 1.08e-05 1000.322 0.000 0.011 0.011
    Omnibus: 180114.225 Durbin-Watson: 0.057
    Prob(Omnibus): 0.000 Jarque-Bera (JB): 441927.253
    Skew: -0.132 Prob(JB): 0.00
    Kurtosis: 4.326 Cond. No. 822.


    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

    Le sommaire de la régression linéaire affiche un coefficient de détermination de 0,703, nous parvenons donc maintenant à expliquer environ 70,3% de la variance grâce à l'ajout de cette variable. Notons que le coefficient de détermination est, ici, ajusté. Analysons maintenant la décomposition de l'explication de la variance du revenu pour RL3.

    In [236]:
    decomposition_des_variables(RL3)
    
    Out[236]:
    Pourcentage de l'explication de la variance
    log_mj 64.33
    gini 0.85
    c_i_parents 5.12
    Residual 29.70

    En effet, la classe de revenus des parents explique 5,12% de la variance des revenus des enfants selon RL3. Analysons maintenant les facteurs d'influence de la variance de chacune des 3 variables.

    In [237]:
    VIF(RL3, ["log_mj", "gini", "c_i_parents"])
    
    Out[237]:
    VIF de la variable
    log_mj 1.095369
    gini 1.095369
    c_i_parents 1.000000

    Les VIF étant tous inférieurs à 10, nous ne détectons donc aucune problème de colinéarité. Observons maintenant la droite de Henry liée aux résidus de cette dernière régression linéaire.

    In [238]:
    fig, ax = plt.subplots(1, 1, figsize=(6, 5))
    residus_henry(RL3, "RL3", "darkorange", ax) ; plt.show()
    

    Les résidus semblent bien suivrent une loi gaussienne selon la droite de Henry, excepté encore une fois pour quelques individus à faible résidus. On remarquera une adéquation à la loi normale encore meilleure que pour RL2. Analysont maintenant l'homoscédasticité de RL3 avec le nuage de variance résiduelle.

    In [239]:
    fig, ax = plt.subplots(1, 1, figsize=(6, 5))
    nuage_residuel(RL3, "RL3", "darkorange", ax) ; plt.show()
    

    Les variances des résidus sont significativement égales selon ce nuage, on en déduit l'homoscédasticité de RL3 et attestons ainsi, de par ce résultat et les précédents, de la fiabilité du modèle de régression linéaire RL3.

    Nous allons maintenant vérifier si d'autres variables présentes dans nos données de base influent sur la variance des revenus des individus.

    In [240]:
    RL4=ols("log_y_child ~ log_mj+gini+c_i_parents+log_y_parents+elasticity+population", data=individus).fit(alpha=nt)
    print(RL4.summary()) ; print("") ; print("")
    print("CE NOUVEAU MODELE EXPLIQUE ENVIRON", round(100*RL4.rsquared, 2), "% DE LA VARIANCE DE log_y_child")
    
                                OLS Regression Results                            
    ==============================================================================
    Dep. Variable:            log_y_child   R-squared:                       0.784
    Model:                            OLS   Adj. R-squared:                  0.784
    Method:                 Least Squares   F-statistic:                 3.513e+06
    Date:                Mon, 18 Jan 2021   Prob (F-statistic):               0.00
    Time:                        20:28:45   Log-Likelihood:            -5.6554e+06
    No. Observations:             5800000   AIC:                         1.131e+07
    Df Residuals:                 5799993   BIC:                         1.131e+07
    Df Model:                           6                                         
    Covariance Type:            nonrobust                                         
    =================================================================================
                        coef    std err          t      P>|t|      [0.025      0.975]
    ---------------------------------------------------------------------------------
    Intercept         0.5699      0.003    203.668      0.000       0.564       0.575
    log_mj            0.1446      0.001    267.341      0.000       0.144       0.146
    gini             -0.2101      0.004    -58.793      0.000      -0.217      -0.203
    c_i_parents      -0.0083    1.6e-05   -517.208      0.000      -0.008      -0.008
    log_y_parents     0.8308      0.001   1462.680      0.000       0.830       0.832
    elasticity       -0.0366      0.002    -22.292      0.000      -0.040      -0.033
    population    -4.533e-11    1.6e-12    -28.275      0.000   -4.85e-11   -4.22e-11
    ==============================================================================
    Omnibus:                   362879.795   Durbin-Watson:                   0.078
    Prob(Omnibus):                  0.000   Jarque-Bera (JB):          1706800.459
    Skew:                          -0.025   Prob(JB):                         0.00
    Kurtosis:                       5.657   Cond. No.                     2.57e+09
    ==============================================================================
    
    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
    [2] The condition number is large, 2.57e+09. This might indicate that there are
    strong multicollinearity or other numerical problems.
    
    
    CE NOUVEAU MODELE EXPLIQUE ENVIRON 78.42 % DE LA VARIANCE DE log_y_child
    

    La pvalue de chaque variable étant égale à 0, elles semblent alors toutes significatives pour le modèle, et ce nouveau modèle explique environ 78,42% de la variance des revenus, soit 8,12% de plus que RL3. Avant d'analyser la décomposition des variables, vérifions d'abord leurs éventuelles colinéarités.

    In [241]:
    VIF(RL4, ["log_mj", "gini", "c_i_parents", "log_y_parents", "elasticity", "population"])
    
    Out[241]:
    VIF de la variable
    log_mj 6.283868
    gini 1.438371
    c_i_parents 3.002115
    log_y_parents 8.668267
    elasticity 1.515660
    population 1.023082

    Nous obtenons des facteurs d'influence plus grands, cette fois-ci, dont certains sont supérieurs à 5, mais aucun n'est inférieur à 10. On constate alors une certaine colinéarité pour quelques variables mais on peut admettre qu'elles n'influent pas sur le modèle. On peut donc observer la décomposition de la variance des revenus expliquée par ces variables.

    In [242]:
    decomposition_des_variables(RL4)
    
    Out[242]:
    Pourcentage de l'explication de la variance
    log_mj 64.33
    gini 0.85
    c_i_parents 5.12
    log_y_parents 8.11
    elasticity 0.00
    population 0.00
    Residual 21.58

    Il est intéressant de noter que le revenu même des parents vient s'ajouter à l'explication de la variance des revenus, avec un score de 8,12% dans notre dernier modèle, et que nous pouvons mettre de côté les variables "population" et "elasticity" selon ce modèle. On peut donc reconsidérer RL4 sans ces deux dernières variables.

    In [243]:
    RL4=ols("log_y_child ~ log_mj+gini+c_i_parents+log_y_parents", data=individus).fit(alpha=nt)
    print("Et ce modèle explique toujours", round(100*RL4.rsquared, 2), "% de la variance des revenus.")
    decomposition_des_variables(RL4)
    
    Et ce modèle explique toujours 78.42 % de la variance des revenus.
    
    Out[243]:
    Pourcentage de l'explication de la variance
    log_mj 64.33
    gini 0.85
    c_i_parents 5.12
    log_y_parents 8.11
    Residual 21.58

    Analysons les résidus pour nous assurer de la validité de cette dernière régression linéaire multiple.

    In [245]:
    analyse_des_residus(RL4, "RL4", "chartreuse") ; plt.show()
    

    Les résidus semblent moins confondues avec la droite de Henry mais présentent tout de même une distribution sensiblement proche de la loi gaussienne, et les variances restent sensiblement proches sur le nuage pour ce qui est de l'homoscédasticité. On considérera alors ce dernier modèle comme étant fiable et nous nous reposerons dessus pour la conclusion.

    En se basant sur notre dernière régression linéaire, on établi ci-dessous la liste descendante des variables explicant la variance des revenus des individus selon leur pourcentage de contribution.

    In [246]:
    decomposition_des_variables(RL4).head(4).sort_values(by="Pourcentage de l'explication de la variance", ascending=False)
    
    Out[246]:
    Pourcentage de l'explication de la variance
    log_mj 64.33
    log_y_parents 8.11
    c_i_parents 5.12
    gini 0.85

    Et nous avons vu que ce modèle explique environ 78,42% de cette variance, ce qui signifie qu'environ 21,58% de cette variance s'explique par des facteurs extérieurs (efforts, chances, etc...).

    En passant au logarithme, nous avons pu expliquer jusqu'à 78,42% de la variance des revenus avec les données présentes dans notre base. En observant les résultats de la mission 4, on peut admettre que le revenu d'un individu est conditionné en priorité par le revenu moyen de son pays d'origine, représentant plus de 70% de cette variance. Cette dernière s'explique au second plan par les conditions financières de leurs parents, d'abord selon leur revenu même, puis de leur classe de revenu au sein de leur pays. Enfin, l'observation de l'indice de Gini est un facteur faible mais qui, après analyse des données précédentes, peut être déterminant. Enfin, tout facteur extérieur à ceux étudiés dans ce projet (effort, chance, potentiel...) contribue à 21,58% de la variance des revenus.

    Dans le scénario du projet, nous travaillons dans une banque où l'on a accès à chacune des quatre variables explicatives définies pour chacun de nos clients: le revenu moyen du pays où ils habitent, leur revenu, leur classe de revenu au sein de leur pays, et l'indice de Gini de leur pays. Voici donc un algorithme qui repose sur notre base de données qui, pour un client donné, prendrait en argument le revenu moyen de son pays, son revenu, sa classe de revenu et l'indice de gini de son pays et qui nous renvoie une estimation du revenu de ses enfants.

    In [247]:
    # On se base uniquement sur les 4 variables en vigueurs présentes dans notre base:
    donnees=individus[["y_child", "mj", "y_parents", "c_i_parents", "gini"]]
    
    # Algorithme préliminaire pour l'algorithme de prédiction:
    def proche(x, L):
        K=(abs(x-L))
        a=min(K)
        dt=pd.concat([L, K], axis=1)
        dt.columns=["vari", "abso"]
        dt.drop_duplicates(inplace=True)
        i=list(dt.loc[dt["abso"]==a]["vari"])[0]
        return(i)
    
    # Algorithme de prédiction:
    def prediction(revenu_moyen_pays, revenu_parent, classe_parent, gini):
        
        rmp=proche(revenu_moyen_pays, donnees["mj"])
        data=donnees.loc[donnees["mj"]==rmp]
        
        rp=proche(revenu_parent, data["y_parents"])
        data=data.loc[data["y_parents"]==rp]
        
        cp=proche(classe_parent, data["c_i_parents"])
        data=data.loc[data["c_i_parents"]==cp]
        
        g=proche(gini, data["gini"])
        data=data.loc[data["gini"]==g]
        
        return(round(data["y_child"].mean(), 2))
    

    Imaginons qu'un client gagne environ 3000 dollars ppa par mois, appartienne au 64ème centile, vit dans un pays où le revenu moyen est de 2500 dollars ppa, et où l'indice de gini est de 0,35. On propose une estimation du revenu de son enfant.

    In [248]:
    prediction(2500, 3000, 60, 0.35)
    
    Out[248]:
    3123.56