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/
Salut,
Merci pour cette présentation de Pandas.
il manque toutefois quelques infos bien pratiques.
Je les note ici en vrac pour ceux qui s’y intéressent.
Lecture de fichiers CSV
pd.read_csv
Lecture de documents JSON
pd.read_json
Ecriture de fichiers CSV
pd.to_csv
Ecriture de fichiers Excel
pd.to_excel
Les tracés (interface de plus haut niveau que Matplotlib)
Les tris (selon différentes colonnes)
Pour aller plus loin je conseille la lecture du livre Python for Data Analysis
Data Wrangling with Pandas, NumPy, and IPython de Wes McKinney l’auteur de Pandas
http://shop.oreilly.com/product/0636920023784.do
Sinon il y a la doc mais c’est moins digeste
http://pandas.pydata.org/pandas-docs/stable/
Merci encore
@Seb : je ne suis pas rentré dans toutes les fonctionnalités de Pandas sinon il faudrait plusieurs articles pour tout couvrir, mais je cite bien les diverses méthodes d’import de données (qui marche aussi en export d’ailleurs) et le traçage de diagrammes avec plot :)
Les avantages que je trouve à pandas dans le traitement de data :
* nommage explicite (f[‘age’] plutot que f[3]) qui est utile lorsqu’on manipule des gros tableaux 2D. Cela evite de se poser la question de ce qu’il y a dans la colonne 23.
* manipulation facile des dates, mais je n’en manipule presque pas.
* création de sous-ensembles qui est plus simple qu’avec un tableau numpy à plusieurs dimensions.
Moi j’ai une question niveau de l’efficacité. Est-ce que les opérations sont aussi rapides qu’avec des tableaux numpy (sum, mean et autre par ex.)?
@Joshua: pandas utilise des tableaux numpy en interne, donc les opérations doivent être grosso modo aussi rapides. C’est fait pour bouffer du chiffre, alors ils font gaffe à l’optimisation. De plus les devs numpy font bien gaffe à ne pas casser les bibliothèques principales qui en dépendent lors de leurs mises à jour, je vois pas mal d’échanges sur github.
Moi ce que j’aime dans pandas c’est le chargement des données. Tu files un tableau excel et tu récupères le tableau direct, avec détection de la ligne d’entête et du type de chaque colonnes. Je l’ai fait une fois à la min avant de découvrir cette lib, j’ai tout de suite vu la différence !
Salut,
j’ai une petite question, comment faire pour lire plusieurs fichiers texte un par un dans un dossier? merci
@LuTarip : cette question n’a rien à voir avec l’article, et ceci est un blog, pas un forum. Va plutôt poser ta question ici : http://www.afpy.org/forums/forum_python