joshuafr – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Le pandas c’est bon, mangez en http://sametmax.com/le-pandas-cest-bon-mangez-en/ http://sametmax.com/le-pandas-cest-bon-mangez-en/#comments Sat, 10 May 2014 06:30:35 +0000 http://sametmax.com/?p=10113 Les bases de Numpy, je m'en vais vous présenter une lib qui roxx du poney dans le calcul numérique : Pandas.]]> Ceci est un post invité de joshuafr posté sous licence creative common 3.0 unported.

Bonjour à tous, jeunes tailleurs de bambou, suite à un article d’introduction à numpy par le grand maître Sam Les bases de Numpy, je m’en vais vous présenter une lib qui roxx du poney dans le calcul numérique : Pandas.

Pour faire simple, Pandas apporte à Python la possibilité de manipuler de grands volumes de données structurées de manière simple et intuitive, chose qui faisait défaut jusqu’ici. Il y a bien eu quelques tentatives comme larry, mais rien n’avait jamais pu égaler les fonctionnalités du langage R. Aujourd’hui Pandas y arrive en fournissant notamment le célèbre type dataframe de R, avec en prime tout un tas d’outils pour agréger, combiner, transformer des données, et tout ça sans se casser le cul. Que du bonheur!

Donc pour commencer, on installe le bousin par un simple : pip install pandas qui va si vous ne l’avez pas déjà fait, aussi télécharger/compiler/installer tout un tas de librairies dont numpy. Je vous conseille aussi d’utiliser ipython afin d’avoir une meilleure interaction avec les libs, notamment avec matplotlib en utilisant le switch ipython --pylab afin d’avoir directement les graphiques en mode interactif, ainsi que toute la bibliothèque numpy directement importée (en interne, ipython fera un import numpy as np).
On appelle la bête d’un simple:

In [1]: import pandas as pd

Oui je sais, la grande classe…

Tout est Series

Le type de base en Pandas est la Series. On peut le voir comme un tableau de données à une dimension:

In [2]: pd.Series(np.arange(1,5))
Out[2]: 
0    1
1    2
2    3
3    4
dtype: int64

La colonne de gauche représente l’index de la Series, normalement unique pour chaque entrée. La colonne de droite correspond à nos valeurs sur lesquelles nous voulons travailler.
L’index n’est pas forcément une suite d’entiers, et la Series peut être nommée:

In [3]: s=pd.Series([1,2,3.14,1e6], index=list('abcd'), name='ma_series')
In [4]: s
Out[4]: 
a          1.00
b          2.00
c          3.14
d    1000000.00
Name: ma_series, dtype: float64

A noter qu’un type-casting est systématiquement appliqué afin d’avoir un tableau de type uniforme (ici le data-type est du float64) qui peut être modifié (dans une certaine mesure) via Series.astype.

Le slicing c’est comme du fisting avec une bonne dose de vaseline, ça glisse tout seul:

In [5]: s['b':'d']
Out[5]: 
b          2.00
c          3.14
d    1000000.00
Name: ma_series, dtype: float64

Et oui, la sélection par indexation se fait sur… l’index de la Series. Ainsi s[‘a’] renverra la ligne dont l’index est ‘a’, mais Pandas est assez intelligent pour reconnaître si on lui demande de nous renvoyer des valeurs suivant l’ordonnancement du tableau de valeurs (comme numpy). Ainsi s[0] renverra la première valeur du tableau, qui ici est égale à s[‘a’].
Là où ça peut poser problème c’est quand notre index est une suite d’entiers, comme par exemple avec x=pd.Series(np.arange(1,5), index=np.arange(1,5)). Si vous demandez x[1], Pandas ne retrouve pas ses petits et vous retournera une zolie KeyError. Pour ces cas ambigus, il existe l’indexation stricte sur le nom de index de la Series via x.loc[nom_d'index], et l’indexation stricte sur le numéro d’ordre dans le tableau via x.iloc[numéro_d'ordre]. Essayez x.loc[0] et x.iloc[0] pour vous rendre compte de la différence.
Comme pour les préliminaires où il est bon de tâter un peu de tout avec de pénétrer dans le vif du sujet, laissons pour le moment l’indexation sur laquelle nous reviendrons plus tard, pour regarder d’un peu plus près comment faire joujou avec nos valeurs.

Un peu à la manière des arrays de numpy, on peut appliquer des fonctions mathématiques directement sur la Serie, ou passer par des fonctions raccourcis:

In [6]: s.sum()
Out[6]: 1000006.14

Ce qui revient au même que de faire np.sum(s) (rappelez vous, ipython avec –pylab a importé numpy dans la variable np).

La fonction describe est bien utile pour avoir un aperçu rapide de ce à quoi on a affaire:

In [7]: s.describe()
Out[7]: 
count          4.000000
mean      250001.535000
std       499998.976667
min            1.000000
25%            1.750000
50%            2.570000
75%       250002.355000
max      1000000.000000
Name: ma_series, dtype: float64

ce qui donne le nombre de données, la moyenne globale, la déviation standard, le minimum, les quartiles et le maximum de la Serie.

Le truc à retenir est que c’est l’index qui est primordial dans un grand nombre d’opérations. Ainsi si l’on veut additionner 2 Series ensemble, il faut que leurs index soient alignés :

In [8]: s2=pd.Series(np.random.rand(4), index=list('cdef'), name='autre_serie')

In [9]: s+s2
Out[9]: 
a               NaN
b               NaN
c          4.021591
d    1000000.401511
e               NaN
f               NaN
dtype: float64

Ici, seuls les index ‘c’ et ‘d’ étaient présents dans les 2 Series, Pandas effectuant avant l’opération d’addition une union basée sur l’index. Les autres entrées sont marquées en NaN, soit Not a Number. Une possibilité pour contrer ce phénomène et de dire à Pandas de remplacer les résultats manquants lors de l’union par 0:

In [10]: s.add(s2, fill_value=0)
Out[10]: 
a          1.000000
b          2.000000
c          4.021591
d    1000000.401511
e          0.563508
f          0.655915
Name: ma_series, dtype: float64

Mais si ce sont uniquement les valeurs qui nous intéressent, et non les indexations, il est possible de les supprimer:

In [11]: s.reset_index(drop=True)+s2.reset_index(drop=True)
Out[11]: 
0          1.881591
1          2.401511
2          3.703508
3    1000000.655915
dtype: float64

Oh joie, oh bonheur, je peux faire ce que je veux avec mes cheveux, enfin mes données…

Et PAN! dans ta frame

La DataFrame est l’extension en 2 dimensions des Series. Elle peut être vue comme un empilement de Series dont les index sont partagés (et donc intrinsèquement alignés), ou comme dans un tableur où les index sont les numéros de lignes et les noms des Series les noms des colonnes. Je ne vais pas décrire toutes les manières de créer une DataFrame, sachez juste qu’on peut les obtenir à partir de dictionnaires, de liste de liste ou de liste de Series, d’arrays ou de records numpy, de fichier excel ou csv et même depuis des bases de données, de fichier JSON ou HTML, et depuis le presse-papiers.

In [14]: genre=[['femme','homme'][x] for x in np.random.random_integers(0,1,100)]

In [15]: lateral=[['droite','gauche'][x] for x in np.random.random_integers(0,1,100)]

In [16]: age=np.random.random_integers(1,100,100)

In [17]: df=pd.DataFrame({'Genre':genre, 'Lateral':lateral, 'Age':age})

In [18]: df
Out[18]: 
    Age  Genre Lateral
0    69  femme  droite
1    46  homme  droite
2    89  homme  droite
3    14  homme  droite
4    74  homme  droite
5     5  femme  gauche
6    66  femme  droite
7    73  homme  gauche
8    99  homme  gauche
9    17  homme  gauche
    ...    ...     ...

[100 rows x 3 columns]

L’affichage par défaut depuis la version 0.13 est en mode ‘truncate’ où la fin de la DataFrame est tronquée suivant la hauteur de votre terminal, mais ça peut se changer via les divers paramètres à regarder sous pd.options.display.

Là donc nous avons une DataFrame de 3 colonnes (plus un index), chaque colonne étant en réalité une Serie :

In [20]: type(df['Age'])
Out[20]: pandas.core.series.Series

La sélection peut se faire de plusieurs manières, à chacun de choisir sa préférée (moi c’est Dafnée avec ses gros nénés). Ainsi pour avoir les 3 premières lignes des âges

In [21]: df['Age'][0:3]
Out[21]: 
0    69
1    46
2    89
Name: Age, dtype: int64

In [22]: df[0:3]['Age']
Out[22]: 
0    69
1    46
2    89
Name: Age, dtype: int64

In [23]: df.Age[0:3]
Out[23]: 
0    69
1    46
2    89
Name: Age, dtype: int64

In [24]: df.loc[0:3, 'Age']
Out[24]: 
0    69
1    46
2    89
3    14
Name: Age, dtype: int64

et oui, les noms de colonnes peuvent aussi être utilisés comme des attributs de la DataFrame. Pratique (qu’on n’attend pas).

L’une des forces de Pandas est de nous proposer tout un tas de solutions pour répondre à des questions existentielles tel que “quel est l’âge moyen par genre et par latéralité?”. Comme en SQL où la réponse sortirait du fondement d’une clause GROUP BY et d’une fonction d’agrégation, il en va de même ici :

In [25]: df.groupby(['Genre','Lateral']).aggregate(np.mean)
Out[25]: 
                     Age
Genre Lateral           
femme droite   45.476190
      gauche   49.208333
homme droite   41.571429
      gauche   55.823529

[4 rows x 1 columns]

OMG! c’est quoi c’t’index de malade? Un MultiIndex jeune padawan, qui te permettra d’organiser tes données par catégorie/niveau, et d’y accèder par le paramètre level dans pas mal de fonctions, mais ça je te laisse le découvrir par toi-même. Je ne vais pas non plus m’étendre plus sur toutes les possibilités offertes par les DataFrame, il y a tellement à dire qu’il faudrait plusieurs articles pour en faire le tour. Juste conclusionner sur la facilité d’intégration Pandas/matplotlib en vous disant que les Series et DataFrame ont une fonction plot permettant directement de visualiser les données, et ça, c’est juste jouissif.

Datetime dans les index

Je vous avez dit qu’on reviendrait sur les indexes, et là c’est pour rentrer dans le lourd (mais non pas toi Carlos). Pandas donc supporte l’indexation sur les dates, en reprenant et en élargissant les possibilités offertes par feu le module scikits.timeseries.
Prenons l’exemple de données (complètement bidons) fournies par un capteur à intervalle régulier sur un pas de temps horaire:

In [26]: dtindex=pd.date_range(start='2014-04-28 00:00', periods=96, freq='H')

In [27]: data=np.random.random_sample(96)*50

In [28]: df=pd.DataFrame(data, index=dtindex, columns=['mesure'])

In [29]: df.head()
Out[29]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 01:00:00   1.910280
2014-04-28 02:00:00   7.534761
2014-04-28 03:00:00  39.416415
2014-04-28 04:00:00  44.213409

[5 rows x 1 columns]

In [30]: df.tail()
Out[30]: 
                        mesure
2014-05-01 19:00:00  25.291453
2014-05-01 20:00:00  26.520291
2014-05-01 21:00:00  33.459766
2014-05-01 22:00:00  44.521813
2014-05-01 23:00:00  28.486003

[5 rows x 1 columns]

dtindex est un DatetimeIndex initialisé au 28 avril 2014 à 0 heure comportant 96 périodes de fréquence horaire, soit 4 jours. La fonction date_range peut aussi prendre en arguments des objets datetime purs au lieu de chaine de caractère (manquerait plus que ça…), et le nombre de périodes peut être remplacé par une date de fin.
Si l’on veut calculer, disons le maximum (horaire) par jour, rien de plus simple, il suffit de “resampler” en données journalières (‘D’ pour Day) et de dire comment aggréger le tout:

In [31]: df.resample('D', how=np.max)
Out[31]: 
               mesure
2014-04-28  26.298282
2014-04-29  28.385418
2014-04-30  26.723353
2014-05-01  24.106092

[4 rows x 1 columns]

Mais on peut aussi convertir en données quart-horaire (upsampling) en remplissant les données manquantes par celles de l’heure fixe:

In [32]:  df[:3].resample('15min', fill_method='ffill')
Out[32]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00  49.253929
2014-04-28 00:30:00  49.253929
2014-04-28 00:45:00  49.253929
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00   1.910280
2014-04-28 01:30:00   1.910280
2014-04-28 01:45:00   1.910280
2014-04-28 02:00:00   7.534761

[9 rows x 1 columns]

Cependant, Pandas propose aussi d’autres possibilités non dépendantes des DatetimeIndex mais qu’il est bon de connaître, notamment celle pour remplacer les données manquantes avec fillna ou celle pour interpoler entre les données valides avec interpolate

In [52]:  df[:3].resample('15min')
Out[52]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00        NaN
2014-04-28 00:30:00        NaN
2014-04-28 00:45:00        NaN
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00        NaN
2014-04-28 01:30:00        NaN
2014-04-28 01:45:00        NaN
2014-04-28 02:00:00   7.534761

[9 rows x 1 columns]

In [53]:  df[:3].resample('15min').fillna(df.mean())
Out[53]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00  26.378286
2014-04-28 00:30:00  26.378286
2014-04-28 00:45:00  26.378286
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00  26.378286
2014-04-28 01:30:00  26.378286
2014-04-28 01:45:00  26.378286
2014-04-28 02:00:00   7.534761

[9 rows x 1 columns]

In [54]:  df[:3].resample('15min').interpolate()
Out[54]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00  37.418016
2014-04-28 00:30:00  25.582104
2014-04-28 00:45:00  13.746192
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00   3.316400
2014-04-28 01:30:00   4.722520
2014-04-28 01:45:00   6.128641
2014-04-28 02:00:00   7.534761

[9 rows x 1 columns]

Voilà, j’espère que vous aurez plaisir à travailler avec cette librairie, il manquait vraiment un outil de cette trempe en Python pour l’analyse de données et je pense qu’on n’a plus trop grand chose à envier maintenant par rapport à des langages spécilisés. Je n’ai pas parlé de Panel qui est le passage à la troisième dimension, ni des possibilités d’export, notamment la df.to_html que je vous laisse le soin de découvrir.

A plus, et amusez vous bien avec votre bambou.

\o/

]]>
http://sametmax.com/le-pandas-cest-bon-mangez-en/feed/ 7 10113