Data Science en pratique et insertion professionelle

Arthur Llau, data scientist chez Safety Line : arthur.llau@safety-line.fr

Prochain cours les 16 et 30 Novembre, puis le 4 Décembre.

Cours 2 : Variables catégorielles, et valeurs manquantes.

Objectif du cours:

  • Présenter les variables catégorielles, comment les visualiser et commment les manipuler
  • Donner des stratégies pour compléter des valeurs manquantes.
  • TP de mise en pratique

Variables catégorielles

Les variables catégorielles sont des variables qualitatives qui caractérisent un attribut d'une observation. Par exemple, une variable catégorielle pour une voiture est sa couleur. Certaines de ces variables peuvent être numériques et ordonnées, elles sont dites ordinales comme la note d'un étudiant à un examen.

1. Comment les réperer et les visualiser ?

Les données utilisées pour illuster cette section proviennent d'un Kaggle "éducation" que vous pouvez trouver à l'adresse suivante https://www.kaggle.com/c/ghouls-goblins-and-ghosts-boo.

1.1 - Repérage

In [2]:
data = pd.read_csv('ghouls.csv')
print data.head()
# On repère rapidemment deux variables catégorielles: color & type
   id  bone_length  rotting_flesh  hair_length  has_soul  color    type
0   0     0.354512       0.350839     0.465761  0.781142  clear   Ghoul
1   1     0.575560       0.425868     0.531401  0.439899  green  Goblin
2   2     0.467875       0.354330     0.811616  0.791225  black   Ghoul
3   4     0.776652       0.508723     0.636766  0.884464  black   Ghoul
4   5     0.566117       0.875862     0.418594  0.636438  green   Ghost
In [3]:
#La commande .info() permet d'afficher des informations sur les variables d'un dataframe pandas 
data.info() 
# Object signifie string, donc par forcément catégorielle.
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 371 entries, 0 to 370
Data columns (total 7 columns):
id               371 non-null int64
bone_length      371 non-null float64
rotting_flesh    371 non-null float64
hair_length      371 non-null float64
has_soul         371 non-null float64
color            371 non-null object
type             371 non-null object
dtypes: float64(4), int64(1), object(2)
memory usage: 20.4+ KB

Il est parfois nécéssaire (selon la librairie utilisée) de transformer les variables "object" représentant une catégorie en variables catégorielles (category).

In [4]:
data['color']=data['color'].astype("category")
data['type']=data['type'].astype("category")
print data.info() ## Ce sont désormais bien des catégories
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 371 entries, 0 to 370
Data columns (total 7 columns):
id               371 non-null int64
bone_length      371 non-null float64
rotting_flesh    371 non-null float64
hair_length      371 non-null float64
has_soul         371 non-null float64
color            371 non-null category
type             371 non-null category
dtypes: category(2), float64(4), int64(1)
memory usage: 15.6 KB
None

Pour réperer que ce sont bien des variables catégorielles, on peut compter le nombre d'éléments uniques dans la variable (si on ne dispose pas d'informations sur les features). Attention, si ce dernier est trop élevé ce n'est peut être pas des variables catégorielles.

In [5]:
print data['color'].unique()
print '\n', data['color'].value_counts(normalize = True) #Normalize = True affiche les fréquences, False le nombre
[clear, green, black, white, blue, blood]
Categories (6, object): [clear, green, black, white, blue, blood]

white    0.369272
clear    0.323450
green    0.113208
black    0.110512
blue     0.051213
blood    0.032345
Name: color, dtype: float64
In [6]:
print '\n', data['type'].value_counts(normalize = True) #Normalize = True affiche les fréquences, False le nombre
Ghoul     0.347709
Goblin    0.336927
Ghost     0.315364
Name: type, dtype: float64

Trois méthodes graphiques particulières permettent d'afficher des informations sur les variables catégorielles : le countplot, le barplot et le boxplot.

  • Le countplot affiche la fréquence d'une variable
  • Le barplot permet d'afficher une variable catégorielle contre une autre non catégorielle
  • Le boxplot montre le profil d'une variable contre une autre, c'est une visualisation de la distribution empirique associée.

Les figures ci-dessous sont réalisées avec seaborn, qui est beaucoup plus intuitif à utiliser que matplotlib.

In [7]:
## Countplot
plt.subplot(121)
sns.countplot(x='color',data = data); 
plt.subplot(122)
sns.countplot(x='type',data = data);
In [8]:
# Un avantage de seaborn permet d'afficher un plot en fonction d'une variable !
sns.countplot(x='color',hue='type',data = data);
In [9]:
#Barplot, la barre noire représente l'écart type
plt.subplot(121)
sns.barplot(x='color',y='bone_length',data = data); 
plt.subplot(122)
sns.barplot(x='type',y='bone_length',data = data);
In [10]:
# De la même manière que le countplot !
sns.barplot(x='color',y='bone_length',hue='type',data = data); #La barre noire représente l'écart type
In [11]:
#Boxplot
sns.boxplot(x='color',y='bone_length',hue='type',data = data);

2. Comment gérer les variables catégorielles ?

Les librairies de machine learning ne permettent généralement pas d'apprendre ou de réaliser des transformations avec des variables catégorielles. Si vous avez des variables object ou category l'erreur suivante apparaitra :

ValueError: could not convert string to float: [variable], il faut alors les transformer.

Notez qu'il existe tout de même quelques implémentations d'algorithmes tenant compte des variables de type category comme nous le verrons dans le cours 6.

2.1 Label Encoder

C'est la façon la plus naïve de procéder, on va donner un nombre entier correspondant à chaque catégorie. Par exemple, on a trois types de monstres, on pourrait poser Ghost = 0, Ghoul = 1, Goblin = 2. La fonction LabelEncoder de sklearn permet de réaliser cette opération.

In [12]:
from sklearn.preprocessing import LabelEncoder

Encoder = LabelEncoder()
new_type = Encoder.fit_transform(data['type']) #Apprend et transforme les variables catégorielles
print new_type[:5]
print Encoder.inverse_transform(new_type)[:5] # Permet de retransformer les variables. 
#Notons que cela s'applique de manière alphabétique
[1 2 1 1 0]
['Ghoul' 'Goblin' 'Ghoul' 'Ghoul' 'Ghost']

2.2 N-uplet Encoder

N-uplet Encoder permet de transformer une séquence de variables catégorielles en une seule variable. Par exemple, si il y a redondance entre la couleur et le type d'un monstre pourquoi ne pas le définir comme une seule et même variable ? Cela inclut une réduction de dimension qui peut parfois s'avérer cruciale pour les modèles. On peut bricoler cette méthode de la façon suivante.

In [13]:
color_type = data['color'].astype('object')+str(' ')+data['type'].astype('object') #Concatene les catégories en une
print color_type[:5]
#On peut compter le nombre de couple unique, pour vérifier que ce n'est pas dangereux de faire cette opération 
from collections import Counter
print 'Nombre de couples : ',len(Counter(color_type))
0     clear Ghoul
1    green Goblin
2     black Ghoul
3     black Ghoul
4     green Ghost
dtype: object
Nombre de couples :  18
In [14]:
# On encode alors !
Encoder = LabelEncoder()
new_color_type = Encoder.fit_transform(color_type) #Apprend et transforme les variables catégorielles
print new_color_type[:5]
[10 14  1  1 12]

2.3 Binarisation

La binarisation revient à construire pour chaque catégorie d'une variable, un nouveau feature binaire. Par exemple, plutôt que d'avoir une variable type, on aurait une variable pour chaque type existant, avec 1 si le monstre est tel ou tel type. Cela augmente le nombre de dimensions. Mais cela permet parfois une nette amélioration des performances, surtout sur des modèles basés sur des arbres (Breiman). Get_dummies de pandas permet d'effectuer cette transformation.

In [15]:
print data['color'].head()
print '\n En binarisant cela devient \n'
pd.get_dummies(data['color']).head()
0    clear
1    green
2    black
3    black
4    green
Name: color, dtype: category
Categories (6, object): [black, blood, blue, clear, green, white]

 En binarisant cela devient 

Out[15]:
black blood blue clear green white
0 0 0 0 1 0 0
1 0 0 0 0 1 0
2 1 0 0 0 0 0
3 1 0 0 0 0 0
4 0 0 0 0 1 0

Détérminer la meilleure stratégie n'est pas une chose aisée. Cela dépend de la taille du jeu de données, de l'information apportée par les variables catégorielles et des performances de calcul de vos machines. Une manière de comparer les stratégies est d'effectuer un test de performance avec un modèle simple pour chacune d'entre elles.

Valeurs manquantes

Lors du dernier cours, nous avons vu comment afficher les valeurs manquantes et les compléter de manière naïve. Cette section vise à présenter différentes stratégies plus efficaces pour compléter les valeurs manquantes.

1. Imputation par une statistique ou une valeur quelconque

On peut compléter de manière simple des données manquantes par des statistiques comme la moyenne, la médiane ou la valeur la plus fréquente. Cette méthode reste cependant très minimaliste et ne permet pas vraiment de gain d'information. Remplir toutes les valeurs manquantes par une même valeur ne permet généralement pas d'améliorer le résultat. Imputer de sklearn et fillna de pandas permettent de réaliser cette imputation.

In [17]:
# Pour illustrer remplaçons de manière des observations aléatoire par des nan
ix = np.random.randint(0,data.shape[0],50)
missing_data = data['bone_length']
for i in ix:
    missing_data.loc[i] = np.nan
    
plt.plot(data.bone_length,c='b')
mean_fill = missing_data.fillna(np.mean(data['bone_length']))
plt.scatter(ix,mean_fill.iloc[ix],c='r')

#on remarque bien que cela comble les trous mais n'apporte que très peu d'informations
Out[17]:
<matplotlib.collections.PathCollection at 0x7f9e59440b50>

2. Imputation par propagation

Il existe des méthodes d'imputation par propagation avant ou arrière, elles peuvent être pertinentes dans le cas de données temporelles ou de données continuelles. Par exemple, pour compléter une vitesse en fonction de la distance etc... Si il y a une certaine périodicité cette méthode peut également être utile. Imputer de sklearn et fillna de pandas permettent de réaliser cette imputation.

In [18]:
# Cas forward/backward, on remplace par la valeur suivante/précedente. 
plt.plot(data.bone_length,c='b',linewidth=5,label='Missing');
ffill = missing_data.fillna(method='ffill')
plt.plot(ffill,c='r',label='ffill');


bfill = missing_data.fillna(method='bfill')
plt.plot(bfill,c='g',label='bfill');

plt.legend()
#On peut bien évidemment aussi compléter avec les valeurs m
Out[18]:
<matplotlib.legend.Legend at 0x7f9e59838710>

3. Imputation par régréssion (et apprentissage).

Si les distributions des variables le permettent, on peut compléter les valeurs manquantes en effectuant de l'apprentissage. Il faut cependant choisir très soigneusement ces variables. Nous reviendrons sur cette méthode dans un prochain cours.

In [19]:
#Pour ploter une variable contre l'autre il est nécéssaire de remplir les na de manière naive
tmp_data = data.fillna(1) 
sns.pairplot(tmp_data)
Out[19]:
<seaborn.axisgrid.PairGrid at 0x7f9e59729150>
In [21]:
plt.plot(true_value,c='b',linewidth=5,label='Missing');
plt.plot(reg_fill,c='r',label='reg_fill');
plt.legend()
Out[21]:
<matplotlib.legend.Legend at 0x7f9e5016e950>

4. Imputation par interpolation et splines.

L'idée de l'imputation par interpolation ou par spline repose sur la théorie du signal. La périodicité du signal permet d'interpoler les valeurs pour reconstruire un signal bruité (marche très bien avec les séries temp). Il en va de même pour les splines. Nous verrons ces méthodes plus en détails au cours suivant, car cela permet de lisser des signaux pour la visualisation. La librairie scipy possède les fonctions nécéssaires à ces méthodes d'imputation.

In [22]:
from IPython.display import Image
Image(filename = 'altbefore.png', width=300, height=300)
Out[22]:
In [23]:
Image(filename = 'altafter.png', width=300, height=300)
Out[23]:

5. Imputation par K-NN.

Dans le jeu de données précédent on a remarqué quelques similitudes entre les observations, par exemple avec la redondance de certains couples de variables catégorielle, n'existe-t-il pas alors une façon de compléter des valeurs manquantes par similarité ?

Oui, les k-plus proches voisins ! C'est un algorithme très utilisé en pratique lorsqu'on regarde des observations indépendantes comme des contrats ou des objets sur une chaine de production, par exemple. L'idée est de donner un degré de similarité entre plusieurs observations. On peut par exemple, compléter les valeurs manquantes en choisisant les cinq observations les plus similaires. Pour notre jeu de données, si la couleur d'une observation était absente, nous pourrions la retrouver en choissisant les observations avec des caractéristiques similaires.

C'est un algorithme très puissant, mais qui peut être très coûteux.

Conclusion

On a vu qu'il existait diverses méthodes pour compléter les valeurs manquantes, cependant toutes ne sont pas efficaces et dépendent grandement du contexte des données. Il existe également des méthodes beaucoup plus complexes comme l'IterativeSVD, ou MICE. Vous verrez quelques-unes de celles-ci dans le cours de Claire Boyer au second semestre. Si cela vous intéresse, vous pouvez regarder le challenge Netflix et les solutions qui y sont apportées (http://blog.echen.me/2011/10/24/winning-the-netflix-prize-a-summary/).

TP - Kaggle : Bike Sharing Demand

Plus d'infos sur https://www.kaggle.com/c/bike-sharing-demand

In [24]:
import pandas as pd, seaborn as sns, numpy as np, matplotlib.pyplot as plt # Les imports importants !
%matplotlib inline

1 Exploratory Data Analysis

Deux jeux de données: le jeu d'apprentissage du 01/01/11 au 31/07/12 et le jeu de test du 1/08/12 au 31/12/12.

La variable à prédire est la variable count

1.1 Importer les données, parser la variable datetime de manière à obtenir 4 nouvelles variables : year,month,day & hour. Observer un peu le jeu de données train.

In [25]:
train = pd.read_csv('train.csv', parse_dates=['datetime'])

dt = pd.DatetimeIndex(train['datetime'])
train['year'] = dt.year
train['month'] = dt.month
train['day']=dt.weekday #0=lundi ...
train['hour'] = dt.hour

test = pd.read_csv('test.csv',parse_dates=['datetime'])
dt = pd.DatetimeIndex(test['datetime'])
test['year'] = dt.year
test['month'] = dt.month
test['day']=dt.weekday 
test['hour'] = dt.hour
In [26]:
train.head(5) # Tiens des variables catégorielles et des valeurs manquantes ...
Out[26]:
datetime season holiday workingday weather temp atemp humidity windspeed count year month day hour
0 2011-01-01 00:00:00 winter 0 0 clear 9.84 14.395 81 5.18 16 2011 1 5 0
1 2011-01-01 01:00:00 NaN 0 0 clear 9.02 13.635 80 NaN 40 2011 1 5 1
2 2011-01-01 02:00:00 NaN 0 0 clear 9.02 13.635 80 6.32 32 2011 1 5 2
3 2011-01-01 03:00:00 winter 0 0 clear 9.84 14.395 75 NaN 13 2011 1 5 3
4 2011-01-01 04:00:00 winter 0 0 clear 9.84 14.395 75 NaN 1 2011 1 5 4
In [27]:
train.describe()
Out[27]:
holiday workingday temp atemp humidity windspeed count year month day hour
count 8607.000000 8607.000000 7857.000000 8607.000000 8607.000000 6943.000000 8607.000000 8607.000000 8607.000000 8607.000000 8607.000000
mean 0.027768 0.682816 19.961265 23.382848 60.951667 14.515278 173.044150 2011.370048 5.600558 3.014872 11.551644
std 0.164317 0.465407 7.956268 8.667507 19.727106 7.152076 165.189186 0.482845 3.228618 2.000816 6.914326
min 0.000000 0.000000 0.820000 0.760000 0.000000 3.260000 1.000000 2011.000000 1.000000 0.000000 0.000000
25% 0.000000 0.000000 13.940000 15.910000 46.000000 8.998100 37.000000 2011.000000 3.000000 1.000000 6.000000
50% 0.000000 1.000000 20.500000 24.240000 60.000000 12.998000 129.000000 2011.000000 5.000000 3.000000 12.000000
75% 0.000000 1.000000 26.240000 31.060000 77.000000 19.120000 257.500000 2012.000000 8.000000 5.000000 18.000000
max 1.000000 1.000000 41.000000 45.455000 100.000000 56.996900 873.000000 2012.000000 12.000000 6.000000 23.000000
In [28]:
train.hist(figsize=(20, 20), bins=50, layout=(7, 6)); #Pas mal de features catégorielles. 

1.2 Regardez la variable cible count en fonction des autres variables pour voir celles qui semblent influer. Pensez à réaliser des boxplots, et autres figures apportant de l'information.

In [29]:
fig,axs = plt.subplots(nrows=2,ncols=2,figsize=(15,8))
fig.subplots_adjust(hspace=.3,wspace=.3)
sns.boxplot(x='year',y='count',data=train,ax=axs[0][0]);
sns.boxplot(x='month',y='count',data=train,ax=axs[0][1]);
sns.boxplot(x='day',y='count',data=train,ax=axs[1][0]);
sns.boxplot(x='hour',y='count',data=train,ax=axs[1][1]);
In [30]:
fig,axs = plt.subplots(nrows=2,ncols=2,figsize=(15,8))
fig.subplots_adjust(hspace=.3,wspace=.3)
sns.boxplot(x='season',y='count',data=train,ax=axs[0][0]);
sns.boxplot(x='workingday',y='count',data=train,ax=axs[0][1]);
sns.boxplot(x='weather',y='count',data=train,ax=axs[1][0]);
In [31]:
fig,axs = plt.subplots(nrows=2,ncols=2,figsize=(15,8))
fig.subplots_adjust(hspace=.3,wspace=.3)
sns.regplot(y='windspeed',x='count',data=train,ax=axs[0][0],fit_reg=False);
sns.regplot(y='atemp',x='count',data=train,ax=axs[0][1],fit_reg=False);
sns.regplot(y='temp',x='count',data=train,ax=axs[1][0],fit_reg=False);
sns.regplot(y='humidity',x='count',data=train,ax=axs[1][1],fit_reg=False);