Exercice: Cytomégalovirus humain en Ouganda (et intro à pandas)¶
Ceci est un exemple de README.ipynb, un notebook que je vous conseille de créer dès que vous recevez un nouveau jeu de données. Le but est de centraliser au même endroit:
- Une notice de la provenance et du contenu des données, comment les récupérer à nouveaux
- Une visualisation rapide du contenu (descriptive seulement, vous pouvez prendre des notes en markdown), ici j'ai juste fait des commentaires sur pandas.
- Une vérification de la cohérence des données
- Quelques transformations préalables à l'analyse
- Bien séparer ce qui vous arrive (dans
raw/
) et ce qui va servir à vos analyses après vérification/corrections (dansprocessed/
)
Le Cytomégalovirus humain, (HCMV) ou Human Herpesvirus 5 est un virus de la famille des herpès virus très fréquent dans la population humaine. Voici un jeu de donnée permettant de caractériser l'interaction du HCMV avec d'autres infections transmissibles comme le VIH, la tuberculose et des facteurs de risques cardiovasculaires.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
Origine des données¶
Les données dans raw ont été fournies par Guillaume Louvel lors du cours de statistique le 2x novembre 2018.
Ces données sont issues de l'article:
Stockdale, Lisa, Stephen Nash, Angela Nalwoga, Hannah Painter, Gershim Asiki, Helen Fletcher, and Robert Newton. “Human Cytomegalovirus Epidemiology and Relationship to Tuberculosis and Cardiovascular Disease Risk Factors in a Rural Ugandan Cohort.” PloS One 13, no. 2 (2018): e0192086. Lien vers PlosOne
Le jeu de donnée original peut être récupéré sur dryad:
Stockdale, Lisa, Stephen Nash, Angela Nalwoga, Hannah Painter, Gershim Asiki, Helen Fletcher, and Robert Newton. “Data from: Human Cytomegalovirus Epidemiology and Relationship to Tuberculosis and Cardiovascular Disease Risk Factors in a Rural Ugandan Cohort,” February 6, 2018. https://doi.org/10.5061/dryad.d1k17. Lien vers Dryad
%%bash
# On peut mettre un script bash dans un notebook !
# Créer l'arborescence
mkdir -p data/raw
# Récupérer les données depuis le dossier du projet.
cp /users/mag/tpinfo/projet_HCMV.csv data/raw/
# Récupérer les données depuis la source
wget https://datadryad.org/bitstream/handle/10255/dryad.170089/RAW_DATA.xls?sequence=1 -O data/raw/RAW_DATA.xls
# On peut aussi faire une seule commande bash dans le notebook avec le préfixe "!".
!head data/raw/projet_HCMV.csv
Notice des variables¶
Nom | Description |
---|---|
sex |
Sexe des individus ('Male', 'Female') |
cmv |
Dosage d'anticorps au HCMV |
age |
Age en année |
cmvstatus |
Séropositivité au CMV ('positive', 'negative') |
hiv |
Séropositivié au HIV |
TB |
Résultat du test salivaire à la tuberculose |
m_syst |
Durée moyenne de systole |
m_diast |
Durée moyenne de diastole |
R22_bpgroup |
Hypertension |
BMI |
Indice de masse corporelle (IMC) |
cholesterol |
Taux sanguin de cholesterol |
high_d_lipid |
Taux sanguin de HDL |
low_d_lipid |
Taux sanguin de LDL |
hba1c |
Taux d'hémoglobine glyquée |
cmv_tertile |
Catégorisation en 3 quantiles de cmv |
Lecture¶
# Ouvrir le fichier
data = pd.read_csv('data/raw/projet_HCMV.csv', sep=';', decimal=',') #=> Un objet de type pd.DataFrame
data = pd.read_excel('data/raw/RAW_DATA.xls')
print("Tableau de donnée de taille: {} lignes, {} colonnes ".format(*data.shape))
data.head()
# On peut boucler sur les noms de colonnes avec:
for col in data.columns:
print("- {} ({})".format(col,data[col].dtype), end='') # col.dtype donne le type de la colonne.
n = data[col].nunique() # nunique compte le nombre de valeurs uniques dans la colonne, .unique() liste ces valeurs
print(' {} unique values {}'.format(n, ': '+str(data[col].unique()) if n <= 4 else ''))
# pandas utilise un type nommé catégorie pour les données qualitatives (comme les factors de R)
# pd. read stocke les entrées des csv comme des chaines de caractère (dtype=object par défaut)
# mais on peut les convertir en catégorie avec:
for col in ['sex', 'hiv', 'cmvstatus', 'cmv_tertile']:
data[col] = data[col].astype('category')
# On peut accéder au np.array sous-jacent de chaque colonne.
data.cmv.values #=> np.array
#On peut filtrer grace au "Boolean indexing", comme pour les np.array.
data[data.cmv>2.5]
# On remarque que les colonnes arge_yrs, mean_syst et mean_diast qui devaient être float
# sont de type object.
# La colonne "age" contient des valeurs qui ne sont pas des nombres...
# On le voit en faisant data['age_yrs'].unique()
print(frozenset([type(x) for x in data['age_yrs']]))
data['age_yrs'].unique()
# On peut le corriger comme ceci
# On créer une colonne age, numérique cette fois ci. # (on fixe tout 80+ à 80)
# Ajouter une colonne se fait simplement en assignant un itérable de la bonne longueur.
data['age'] = [a if a!='80+' else 80 for a in data.age_yrs]
# Les colonnes mean_diast et mean_syst contient un "don't know"
data[data.mean_diast=="Don't know"]
# Correction avec une compréhension de liste:
for d in ('diast','syst'):
data['m_'+d]= [float(m) if m!="Don't know" else np.nan for m in data['mean_'+d]]
# La méthode describe donne le nombre de valeur non nulles,
# la moyenne, l'écart type et les pourcentiles des variables quantitatives
data.describe()
# On remarque que le jeu de donnée contient une ligne où cmv est négatif
# (alors que les auteurs ont dit que ce n'était pas le cas)
data[data.cmv<0]
# On le corrige avec un indexage booléen
# Le .copy assure qu'on est entrain de travailler sur une copie et pas une vue de raw.
data = data[data.cmv>0]
data.shape
cols = ['sex', 'hiv', 'cmvstatus', 'cmv_tertile']
fig, axes = plt.subplots(1,len(cols), figsize=(len(cols)*4,4))
# notez l'utilisation de zip pour pouvoir boucler sur les éléments de cols et de axes à la fois.
for ax,col in zip(axes, cols):
df = pd.value_counts(data[col]) #=> pd.DataFrame
print(df, end='\n---\n')
# Les dataframes ont des méthodes de plot qui appellent matplotlib.
# https://pandas.pydata.org/pandas-docs/stable/visualization.html#
df.plot.bar(ax=ax) #=> On peut préciser sur quel ax dessiner.
ax.set(title=col)
import scipy.stats
cols = ['cmv','BMI']
fig, axes = plt.subplots(1,len(cols), figsize=(len(cols)*4,4))
for col,ax in zip(cols,axes):
X = scipy.stats.norm(loc=data[col].mean(), scale=data[col].std())
x = np.linspace(data[col].min(), data[col].max())
ax.plot(x, X.pdf(x))
data[col].plot.hist(ax=ax, normed=True, alpha=0.5, color='C0')
# L'itérateur retourné par groupby permet de séparer le jeu de donnée à partir de la valeur
# d'une colonne.
for sex, df in data[['sex','cmv']].groupby('sex'):
# sex contient la valeur de data.sex (Male/Female)
# df contient un dataframe correspondant à la valeur.
print("=== "+sex+' ===')
print(df.describe())
# L'objet groupby lui même à des méthodes pratiques qui combinent le résultat en un dataframe:
data.groupby('sex').mean() #sum...
# On peut groupby une liste de colonnes:
data.groupby(['sex','cmvstatus']).mean()
# La méthode boxplot peut prendre un argument groupby.
data.boxplot(column='cmv',by='sex');
# Scatter matrix permet d'afficher toutes les paires de variables quantitatives.
from pandas.plotting import scatter_matrix
scatter_matrix(data, figsize=(20,20));
Test de cohérence (sanity check)¶
# On peut conserver seulement certaines colonnes
data = data[['sex', 'cmv', 'age', 'cmvstatus', 'hiv', 'm_diast', 'm_syst',
'R22_bpgroup', 'BMI', 'bmi', 'cholesterol', 'high_d_lipid', 'hba1c',
'low_d_lipid', 'cmv_tertile', ]]
data.shape
# À la fin de la lecture il est important de tester si le jeu de donnée est cohérent
# Si la taille du jeu de donnée est connue à l'avance,
# C'est le bon moment pour la tester
assert data.shape[0] == 2173, 'Nombre de lignes'
assert data.shape[1] == 15, 'Nombre de colonnes'
# Tester si on a pas des valeurs absurdes ou contradictoires...
assert all(data.age>=0), 'Ages négatifs'
assert all(data.cmv>=0), 'cmv negatif'
# Tester si les données sont cohérentes avec la notice:
assert all(data.sex.isin(('Male','Female')))
assert all(data.cmvstatus.isin(('positive','negative'))) #=> is in peut aussi servir pour faire de l'indexage booléen.
Modification du jeu de donnée pour les analyses futures¶
# Ajouter une colonne se fait simplement en assignant un objet pd.Series avec un index compatible.
# On peut catégoriser une variable quantitative avec des seuils
data['age_cat'] = pd.cut(data.age, bins=[0,2,4,6,11,16,21,31,41,51,61,data.age.max()+1],
right=False, include_lowest=True)
# Les pd.Series ont les mêmes règles de "broadcasting" que les np.array
data['log_m_syst'] = np.log(data.m_syst)
data[['age','age_cat', 'm_syst', 'log_m_syst']].tail()
# Enfin il est possible de joindre des tableaux de façon très facile.
data2 = pd.DataFrame({'sex':['Male','Female'], 'test':[1,2]})
# Il suffit de donner le tableau de droite,
# celui de gauche, et le nom de la colonne sur laquelle faire la jointure.
# On peut aussi préciser si c'est une jointure à droite, à gauche, interne ou externe.
# L'explication complète ici: https://pandas.pydata.org/pandas-docs/stable/merging.html#merging
pd.merge(left=data[['sex','cmv']], right=data2, left_on='sex', right_on='sex').head()
Export¶
# Crééons le dossier de sortie avec python.
# Utiliser os et os.path.join permet d'avoir un code multi plateforme (linux, mac, windows.)
import os
out_path = os.path.join('data','processed') #=> "data/processed" sous linux,mac, "data\processed" sous windows.
if not os.path.exists(out_path):
os.mkdir(out_path)
data.to_csv(os.path.join(out_path, 'data.csv'))
!head data/processed/data.csv