From 0edef2e3a7d2a2ed2d63a42f6f8bae830794af3f Mon Sep 17 00:00:00 2001 From: 68d6b36d6dfa73b2f7f25db9fb7770c1 <68d6b36d6dfa73b2f7f25db9fb7770c1@app-learninglab.inria.fr> Date: Thu, 14 Oct 2021 08:11:41 +0000 Subject: [PATCH] Upload .html version of .org file. --- .../document_computationnel.html | 1169 +++++++++++++++++ 1 file changed, 1169 insertions(+) create mode 100644 module3/Paradoxe de Simpson/document_computationnel.html diff --git a/module3/Paradoxe de Simpson/document_computationnel.html b/module3/Paradoxe de Simpson/document_computationnel.html new file mode 100644 index 0000000..e74327e --- /dev/null +++ b/module3/Paradoxe de Simpson/document_computationnel.html @@ -0,0 +1,1169 @@ + + + + + + + +Etude de la nocivité du tabac chez les femmes + + + + + + + + + + + + + +
+

Etude de la nocivité du tabac chez les femmes

+
+

Table of Contents

+ +
+ +
+

1. Préambule

+
+
+
+

1.1. Contexte de l'exercice

+
+

+Dans le cadre du MOOC de l'INRIA sur la recherche reproductible, je +réalise un travail pratique évalué par les pairs. Ce travail pratique +intervient au cours du module 3. Le sujet choisi est le numéro 6, +intitulé : Autour du Paradoxe de Simpson. +

+
+
+ +
+

1.2. Origine des données

+
+

+Nous utilisons les données mises à disposition pour l'exercice par +l'équipe pédagogique du MOOC. Ces données sont un extrait de celles +d'études de l'incidence des pathologies de la thyroïde au sein de la +population britannique. Furent exclus du jeu de donnée +complet : +

+
    +
  • les hommes (n = 1285) ;
  • +
  • les femmes ayant arrêté de fumer (n = 162)
  • +
  • les femmes dont les données n'étaient pas disponibles (n = 18).
  • +
+ +

+URL des données : +

+

+https://gitlab.inria.fr/learninglab/mooc-rr/mooc-rr-ressources/-/raw/master/module3/Practical_session/Subject6_smoking.csv?inline=false +

+ +

+Nous téléchargeons le jeu de donnée de l'exercice au format .csv. Dans +ce jeu de données, chaque ligne correspond aux données d'une +participante lors des études originales. Nous disposons des +informations suivantes : +

+ + + + +++ ++ + + + + + + + + + + + + + + + + + + + + + + +
Nom de colonneDescription de la variable dépendante
SmokerSi la personne fume ou non.
StatusSi la personne est vivante ou décédée.
AgeL'âge de la personne si elle est vivante, l'âge à sa mort si elle est décédée.
+
+
+ +
+

1.3. Bibliothèques et fonctions utilisées

+
+

+Pour la suite des manipulations de données, nous faisons l'usage de +plusieurs fonctions disponibles dans des bibliothèques Python. +

+ +
+
from os.path import exists
+from pandas import read_csv
+from matplotlib import pyplot as plt, rc
+from seaborn import histplot, kdeplot, lmplot
+from urllib import request
+from numbers import Real
+from numpy import nan, arange
+
+
+
+
+ +
+

1.4. Paramètres communs des graphiques

+
+

+Nous souhaitons que les graphiques soient réalisés avec TeX. +

+ +
+
# Draw in LaTeX
+rc('font', **{'family': 'serif', 'serif': ['Courier New']})
+plt.rcParams['text.usetex'] = True
+
+
+
+
+ +
+

1.5. Import des données

+
+

+Dans le cas où une copie locale n'est pas présente, nous téléchargeons +le jeu de données complet depuis son adresse de dépôt. +

+ +
+
data_file = "Subject6_smoking.csv"
+
+if not exists(data_file):
+    request.urlretrieve(data_url, data_file)
+    print('Data set downloaded. \n')
+
+data = read_csv(filepath_or_buffer='Subject6_smoking.csv')
+
+data.info(verbose=True)
+print('\n', data.head())
+
+
+ +
+<class 'pandas.core.frame.DataFrame'>
+RangeIndex: 1314 entries, 0 to 1313
+Data columns (total 3 columns):
+Smoker    1314 non-null object
+Status    1314 non-null object
+Age       1314 non-null float64
+dtypes: float64(1), object(2)
+memory usage: 30.9+ KB
+
+   Smoker Status   Age
+0    Yes  Alive  21.0
+1    Yes  Alive  19.3
+2     No   Dead  57.5
+3     No  Alive  47.1
+4    Yes  Alive  81.4
+
+
+
+ +
+

1.6. Vérifications automatiques des données

+
+

+Vérifions le jeu de données : +

+
    +
  • nous ne devons avoir que les valeurs Yes et No dans la colonne +Smoker ;
  • +
  • seules les catégories ALive et Dead doivent être présentent dans la +colonne Status ;
  • +
  • l'âge doit être un réel entre 0 et 123 (le record de longévité +attesté étant de moins de 123 ans).
  • +
+ +

+En cas de valeur incorrecte, nous remplaçons ladite valeur par nan +(Not a number). +

+ +
+
for row in data.iterrows():
+    if row[1][0] != 'Yes' and row[1][0] != 'No':
+        print(f"Uncorrect value in 'Smoker' column: {row[1][0]} (index = {row[0]})")
+        data['Smoker'].iat[row[0]] = nan
+    if row[1][1] != 'Alive' and row[1][1] != 'Dead':
+        print(f"Uncorrect value in 'Status' column: {row[1][1]} (index = {row[0]})")
+        data['Status'].iat[row[0]] = nan
+    if not isinstance(row[1][2], Real) and not 0 <= row[1][2] <= 123:
+        print(f"Uncorrect value in 'Age' column: {row[1][2]} (index = {row[0]})")
+        data['Age'].iat[row[0]] = nan
+
+
+
+
+ +
+

1.7. Visualisations des données brutes

+
+
+
+

1.7.1. Distribution des âges des participantes

+
+

+Nous souhaitons visualiser les distributions des âges selon les +habitudes de tabagisme. +

+ + +
+

group_ages_kde.png +

+

Figure 1: Distribution des âges des participantes selon les habitudes de tabagisme (estimation par noyau)

+
+ +

+Constatons que les distributions en âge des participantes selon les +habitudes de tabagisme ne correspondent pas : les femmes de plus de 60 +ans sont davantage présentes dans le groupe des non-fumeuses. +

+
+
+
+
+ +
+

2. Analyses de la nocivité du tabagisme chez les femmes

+
+

+Dans les analyses suivantes, nous souhaitons évaluer la nocivité du +tabagisme chez les femmes. Dans le cadre de notre exercice, nous +utilisons trois approches différentes. +

+ +

+Notre hypothèse est que le tabac est nocif chez les femmes. +

+
+ +
+

2.1. Approche 1 | Taux de mortalité des fumeuses et non-fumeuses

+
+

+Nous cherchons à déterminer les taux de mortalité des deux groupes de +notre étude. Nous souhaitons associer ces taux de mortalité à des +intervalles de confiance à 95%. +

+
+ +
+

2.1.1. Calcul des paramètres

+
+

+Commençons par séparer les deux groupes. Le nombre de participantes +mortes dans un groupe ramené à l'effectif total du groupe nous donne +le taux de mortalité dudit groupe. +

+ +
+
rounding = 1
+
+smokers = data[(data['Smoker'] == 'Yes')]
+nonsmokers = data[(data['Smoker'] == 'No')]
+
+dead_smokers = smokers[(smokers['Status'] == 'Dead')]
+dead_nonsmokers = nonsmokers[(nonsmokers['Status'] == 'Dead')]
+
+smokers_death_rate = round(dead_smokers.shape[0] / smokers.shape[0] * 100, rounding)
+nonsmokers_death_rate = round(dead_nonsmokers.shape[0] / nonsmokers.shape[0] * 100, rounding)
+
+
+ +

+Nous déterminons l'intervalle de confiance à 95% avec la formule +suivante : +

+ +
+

+\(\text{IC}_{95} = \frac{100}{n} d \pm 1.96 \sqrt{d}\) +

+ +
+ +

+Où n correspond à l'effectif du groupe considéré et d au nombre +d'individus décédés au sein du même groupe. Voici le code que nous +avons utilisé pour réaliser le calcul des intervalles de confiance : +

+ +
+
def death_rate_ci95(sample_size: int,
+                    deads: int,
+                    per_people: int = 100) -> tuple:
+    """Compute the 95% confidence interval upper and lower limits of a mortality rate in a sample.
+
+    :param sample_size: number of people in the sample.
+    :param deads: number of deads people in the sample.
+    :param per_people: standardisation number: deads for per_people people. 
+    """
+    from math import sqrt
+
+    
+    upper = per_people / sample_size * (deads + 1.96 * sqrt(deads))
+    lower = per_people / sample_size * (deads - 1.96 * sqrt(deads))
+
+    return lower, upper
+
+smokers_dr_95ic = death_rate_ci95(smokers.shape[0], dead_smokers.shape[0])
+nonsmokers_dr_95ic = death_rate_ci95(nonsmokers.shape[0], dead_nonsmokers.shape[0])
+
+
+
+
+ +
+

2.1.2. Résultats

+
+ + + +++ ++ ++ ++ ++ + + + + + + + + + + + + + + + + + + + + + + + + + + +
TabagismeEffectif totalIndividus décédésTaux de mortalité (%)IC95%
Fumeuses58213923.9[19.9;27.9]
Non fumeuses73223031.4[27.4;35.5]
+ + +
+

groups_death_rate.png +

+

Figure 2: Effectifs et taux de mortalité parmi les fumeuses et les non fumeuses.

+
+ +

+Pour le jeu de donnée considéré, le taux de mortalité des fumeuses est +de 23.9 contre +31.4 +pour celui observé chez les non-fumeuses. Ce qui va à l'encontre de +notre hypothèse initiale. Il est possible que l'hétérogénéité +constatée dans la distribution des âges explique ce résultat contre +intuitif. +

+
+
+
+ +
+

2.2. Approche 2 | Taux de mortalité des fumeuses et non-fumeuses selon la classe d'âge

+
+

+Nous étudions désormais le taux de mortalité selon les habitudes de +tabagisme et selon la classe d'âge. Nous considérons quatre classes : +

+
    +
  • [18 ; 34[
  • +
  • [34 ; 54[
  • +
  • [54 ; 64[
  • +
  • [64 ; 123[
  • +
+
+ +
+

2.2.1. Calcul des paramètres et résultats

+
+

+Premièrement nous devons séparer le jeu de donnée selon les classes +d'âge. Puis nous procédons de la même manière que précédemment : nous +identifions les effectifs de chaque groupe, le nombre de décès au sein +des sous-groupes et nous calculons le taux de mortalité associé à son +intervalle de confiance à 95%. +

+ +
+
categories = [(18, 34), (34, 54), (54, 64), (64, 123)]
+
+death_rates = []  # plotting purpose
+dr_uncertainties = []  # plotting purpose
+
+print(f"|Classe d'âge|Tabagisme|Effectif total|Individus décédés|\
+        Taux de mortalité (%)|IC95%|\n"
+      f"|-+-+-+-+-+-|")
+
+for category in categories:
+    for smoker in data['Smoker'].unique():
+        
+        alive, dead = nan, nan
+        
+        if smoker == 'Yes':
+            smoke = 'Fumeuses'
+        else:
+            smoke = 'Non-fumeuses'
+        
+        for status in data['Status'].unique():
+            
+            subset = data[(data['Age'] >= category[0])
+                          & (data['Age'] < category[1])
+                          & (data['Smoker'] == smoker)
+                          & (data['Status'] == status)]
+            
+            if status == 'Alive':
+                alive = subset.shape[0]
+            else:
+                dead = subset.shape[0]
+
+        death_rate = dead / (alive + dead) * 100
+        death_rates.append(death_rate)
+
+        dr_ci95 = death_rate_ci95(alive + dead, dead)
+        dr_uncertainties.append(dr_ci95[1] - death_rate)
+
+        rounded_dr_ci95 = list(dr_ci95)
+        rounded_dr_ci95[0] = round(rounded_dr_ci95[0], rounding)
+        rounded_dr_ci95[1] = round(rounded_dr_ci95[1], rounding)
+        r_dr_ci95 = tuple(rounded_dr_ci95)
+        
+        print(f"|[{category[0]};{category[1]}[|{smoke}|{alive + dead}|\
+                {dead}|{round(death_rate, rounding)}|{r_dr_ci95}|")
+
+
+ + + + +++ ++ ++ ++ ++ ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Classe d'âgeTabagismeEffectif totalIndividus décédésTaux de mortalité (%)IC95%
[18;34[Fumeuses17952.8(0.3, 5.2)
[18;34[Non-fumeuses21962.7(0.5, 4.9)
[34;54[Fumeuses2394117.2(11.9, 22.4)
[34;54[Non-fumeuses199199.5(5.3, 13.8)
[54;64[Fumeuses1155144.3(32.2, 56.5)
[54;64[Non-fumeuses1193932.8(22.5, 43.1)
[64;123[Fumeuses494285.7(59.8, 111.6)
[64;123[Non-fumeuses19516685.1(72.2, 98.1)
+ +
+
fig, ax = plt.subplots()
+
+x_pos = arange(len(categories)) 
+width = 0.35
+
+xlabels = list(map(str, categories))
+for x_l, xlabel in enumerate(xlabels):
+    head, _, tail = xlabel.partition(',')
+    xlabels[x_l] = '[' + head[1:] +  ';' + tail[:-1] + '['
+
+plt.bar(x=x_pos - width / 2,
+        height=death_rates[0:len(death_rates):2],
+        yerr=dr_uncertainties[0:len(death_rates):2],
+        label='Smoker', width=width, edgecolor='k',
+        error_kw=dict(lolims=True))
+
+plt.bar(x=x_pos + width / 2,
+        height=death_rates[1:len(death_rates):2],
+        yerr=dr_uncertainties[1:len(death_rates):2],
+        label='Nonsmoker', width=width, edgecolor='k',
+        error_kw=dict(lolims=True))
+
+# In order to modify the default lolims error bars shape
+for ch in ax.get_children():
+    if str(ch).startswith('Line2D'):
+        ch.set_marker('_')
+        ch.set_markersize(6)
+
+plt.ylabel('Death rate (%)')
+plt.xlabel('Age category')
+plt.xticks(x_pos)
+ax.set_xticklabels(xlabels)
+plt.legend()
+plt.tight_layout()
+plt.savefig("age_categories_death_rate.png")
+
+
+ +

+ +

+ + +
+

age_categories_death_rate.png +

+

Figure 3: Taux de mortalité et IC95% selon les habitudes de tabagisme et la classe d'âge

+
+ +

+Pour chacune de nos classes d'âge, les résultats indiquent que le taux +de mortalité des fumeuses est supérieur à celui des non-fumeuses ; +soit une interprétation des données en contradiction avec la première +analyse sans prise en compte de l'âge. +

+ +

+Outre un biais potentiel lié aux distributions des âges hétérogènes, +d'autres biais sont susceptibles d'exister dans les analyses +précédentes : +

+
    +
  • un biais lié à la non prise en compte de l'âge dans le calcul des +taux de mortalité ;
  • +
  • un biais lié aux choix de catégorisation.
  • +
+

+En somme au moins trois biais peuvent se combiner dans nos études +précédentes et fausser nos interprétations. +

+ +

+Enfin, notons que l'âge semble avoir un effet supérieur au tabac sur +le taux de mortalité puisque l'amplitude des différences entre les +classes d'âge est supérieure à celle entre les habitudes de +tabagisme. En somme : le tabac tue, mais vieillir tue davantage. +

+
+
+
+ +
+

2.3. Approche 3 | Probabilité de décès des fumeuses et non-fumeuses selon l'âge

+
+

+Evitons les deux derniers biais en passant du calcul du taux de +mortalité au calcul de la probabilité de décès selon l'âge et les +habitudes de tabagisme. Pour ce faire nous pouvons utiliser la +régression logistique. +

+
+ +
+

2.3.1. Manipulation des données

+
+

+Plutôt que de disposer des status Alive et Dead, nous voulons qu'ils +soient respectivement codés avec 0 et 1. Nous souhaitons à nouveau +disposer de jeux de données spécifiques à chaque groupe : fumeuses et +non fumeuses. +

+ +
+
deaths = []
+
+for status in data['Status']:
+    if status == 'Alive':
+        deaths.append(0)
+    else:
+        deaths.append(1)
+
+data['Death'] =  deaths
+
+smokers = data[(data['Smoker'] == 'Yes')]
+nonsmokers = data[(data['Smoker'] == 'No')]
+print(smokers.head(), '\n', '\n', nonsmokers.head())
+
+
+ +
+  Smoker Status   Age  Death
+0    Yes  Alive  21.0      0
+1    Yes  Alive  19.3      0
+4    Yes  Alive  81.4      0
+7    Yes   Dead  57.5      1
+8    Yes  Alive  24.8      0 
+ 
+    Smoker Status   Age  Death
+2      No   Dead  57.5      1
+3      No  Alive  47.1      0
+5      No  Alive  36.8      0
+6      No  Alive  23.8      0
+11     No   Dead  66.0      1
+
+
+
+ +
+

2.3.2. Régressions logistiques

+
+
+
    +
  1. Réglages et code de la modélisation
    +
    +

    +Pour effectuer la régression logistique, nous utilisons la +bibliothèque statsmodels. Nous laissons les paramètres par défaut : le +modèle est le logit, la méthode est celle du maximum de vraisemblance +(Maximum Likelyhood Estimation). +

    + +
    +
    def logistic_regression(independent_variable: list,
    +                        dependent_variable: list) -> object:
    +    """Logisitic regression with Logit model and MLE method.
    +
    +    :param independent_variable: the independent variable
    +    :param dependent_variable: the dependent variable
    +    
    +    :return fitted_model: the logisitc regression fitted model.
    +    """
    +    import statsmodels.api as sm
    +
    +    model = sm.Logit(dependent_variable, sm.add_constant(independent_variable))
    +    fitted_model = model.fit()
    +
    +    print('\n', fitted_model.summary())
    +
    +    return fitted_model
    +
    +
    +
    +
  2. + +
  3. Cas des fumeuses
    +
    +
    +
    proba_smokers, model_smokers = logistic_regression(smokers['Age'],
    +                                                   smokers['Death'])
    +
    +
    + +
    +/usr/lib/python3/dist-packages/numpy/core/fromnumeric.py:2495: FutureWarning: Method .ptp is deprecated and will be removed in a future version. Use numpy.ptp instead.
    +  return ptp(axis=axis, out=out, **kwargs)
    +Optimization terminated successfully.
    +         Current function value: 0.412727
    +         Iterations 7
    +
    +                            Logit Regression Results                           
    +==============================================================================
    +Dep. Variable:                  Death   No. Observations:                  582
    +Model:                          Logit   Df Residuals:                      580
    +Method:                           MLE   Df Model:                            1
    +Date:              jeu., 14 oct. 2021   Pseudo R-squ.:                  0.2492
    +Time:                        10:05:54   Log-Likelihood:                -240.21
    +converged:                       True   LL-Null:                       -319.94
    +Covariance Type:            nonrobust   LLR p-value:                 1.477e-36
    +==============================================================================
    +                 coef    std err          z      P>|z|      [0.025      0.975]
    +------------------------------------------------------------------------------
    +const         -5.5081      0.466    -11.814      0.000      -6.422      -4.594
    +Age            0.0890      0.009     10.203      0.000       0.072       0.106
    +==============================================================================
    +
    +
    +
  4. + +
  5. Cas des non-fumeuses
    +
    +
    +
    proba_nonsmokers, model_nonsmokers = logistic_regression(nonsmokers['Age'],
    +                                                         nonsmokers['Death'])
    +
    +
    + +
    +/usr/lib/python3/dist-packages/numpy/core/fromnumeric.py:2495: FutureWarning: Method .ptp is deprecated and will be removed in a future version. Use numpy.ptp instead.
    +  return ptp(axis=axis, out=out, **kwargs)
    +Optimization terminated successfully.
    +         Current function value: 0.354560
    +         Iterations 7
    +
    +                            Logit Regression Results                           
    +==============================================================================
    +Dep. Variable:                  Death   No. Observations:                  732
    +Model:                          Logit   Df Residuals:                      730
    +Method:                           MLE   Df Model:                            1
    +Date:              jeu., 14 oct. 2021   Pseudo R-squ.:                  0.4304
    +Time:                        10:05:54   Log-Likelihood:                -259.54
    +converged:                       True   LL-Null:                       -455.62
    +Covariance Type:            nonrobust   LLR p-value:                 2.808e-87
    +==============================================================================
    +                 coef    std err          z      P>|z|      [0.025      0.975]
    +------------------------------------------------------------------------------
    +const         -6.7955      0.479    -14.174      0.000      -7.735      -5.856
    +Age            0.1073      0.008     13.742      0.000       0.092       0.123
    +==============================================================================
    +
    +
    +
  6. +
+
+ +
+

2.3.3. Visualisation des résultats

+
+
+
fig, ax = plt.subplots()
+ax.set_xlim(15, 100)
+ax.set_ylabel('Death probability')
+ax.set_xlabel('Age')
+
+lmplot(data=data, x='Age', y='Death', hue='Smoker',
+       logistic=True, y_jitter=0.01, truncate=False,
+       markers=['o', 'x'], scatter_kws=dict(alpha=0.3))
+
+plt.tight_layout()
+
+plt.savefig('death_probabilities.png')
+
+
+ +

+ +

+ + +
+

death_probabilities.png +

+

Figure 4: Estimation des probabilités de décès selon l'âge et les habitudes de tabagisme.

+
+ +

+L'analyse graphique des résultats semble indiquer une plus forte +probabilité de décès des femmes fumeuses avant 55 ans. Après cet âge, +les probabilités de décès ne paraissent plus influencées par les +habitudes de tabagisme. Le tabac chez les femmes est donc nocif, +surtout avant 55 ans. +

+
+
+
+
+
+

3. Paradoxe de Simpson

+
+

+Nous sommes face à un paradoxe de Simpson quand la tendance observée +au sein de sous-groupes disparaît ou est inversée quand les +sous-groupes sont combinés. +

+
+ +
+

3.1. Qu'en est-il du paradoxe de Simpson dans notre cas ?

+
+

+Lors de notre première approche, les résultats indiquent que le taux +de mortalité est supérieur chez les non-fumeuses (31.4 contre +23.9). Lors de la deuxième +approche, lors de la prise en compte de l'âge, les résultats indiquent +l'inverse : le taux de mortalité est supérieur chez les +fumeuses. Nous sommes face à un paradoxe de Simpson : la prise en +compte ou non de la variable Age inverse les interprétations que nous +pouvons faire des effets du tabagisme chez les femmes. +

+
+
+ +
+

3.2. Quelles sont les explications dans notre cas ?

+
+

+Il est probable que le paradoxe observé dans le cadre de cet exercice +s'explique en partie par les faits suivants : +

+
    +
  • il existe une différence de 150 individus entre les +effectifs des fumeuses et des non fumeuses, soit environ +11.0 % de l'effectif total, ce qui n'est pas négligeable ;
  • +
  • les distributions en âge au sein des groupes (selon les habitudes de +tabagisme) ne coïncident pas ;
  • +
  • l'âge a un effet supérieur au tabac sur le taux de mortalité.
  • +
+
+
+
+
+
+

Author: Victor

+

Created: 2021-10-14 jeu. 10:06

+

Validate

+
+ + -- 2.18.1