Tutoriel improvisé: Manipuler des données en Python avec pandas

Session de ~2h avec pour objectifs:

  • "démarrage à froid" en Python: installation (Anaconda), prise en main du Notebook IPython [0h30]
  • exploration rapide de Python numérique: calcul avec numpy, et tracés avec Matplotlib [1h]
  • objectif final: charger, manipuler et tracer les données d'un capteur de température et de CO2*, avec pandas [1h]

5 février 2015, 14h-16h30, animé par Pierre Haessig

* mise en abîme: la session a lieu dans la salle où est installée ce capteur...

1) Démarrage

a) Installation de Python

La distribution Ananconda, de Continuum Analytics, contient tous les paquets nécessaires pour une utilisation scientifique classique.

lien direct de téléchargement (300 Mo, gratuit)

→ choisir la version adaptée au système d'exploitation (e.g. 32 vs. 64 bits)

b) Notebook IPython

Sous Windows, dans le Menu Démarrer, lancer "Anaconda/IPython Notebook".

→ Ouvre une fenêtre de commande (le serveur) et une fenêtre de navigateur: l'interface du Notebook avec laquelle on va interagir

tour de l'interface

Cellule de type "Code":

In [1]:
a = 1
a+2
Out[1]:
3

Cellule de type "Markdown":

  • pour y écrire du texte (avec un peu de mise en forme, avec la syntaxe Markdown)
  • et les équations Latex: $\theta = 1$

Aide sur la syntaxe: "Markdown basics"

2) Exploration de Python numérique: Numpy et Matplotlib

Importer des modules (avec un alias court, plus pratique)

In [66]:
import numpy as np
import matplotlib.pyplot as plt

Importer juste un élément:

In [2]:
from numpy import pi
pi
Out[2]:
3.141592653589793

Activer l'affichage des graphiques dans le Notebook (image statique)

In [66]:
%matplotlib inline

Activer l'affichage dans une fenêtre séparé (interactif)

In [ ]:
%matplotlib qt

Un petit tracé Matplotlib

Vecteur de données à tracer

In [48]:
t = np.linspace(0, 4*pi)
y = np.sin(t)

Syntaxe de graphs avec Matplotlib: très proche de Matlab (volontairement)

Galerie d'exemples, très pratique pour découvrir: http://matplotlib.org/gallery.html

In [67]:
plt.plot(t, y) # affiche y=f(t)
plt.title('sinus')
plt.xlabel('temps (s)')
plt.grid(True)

3) Lecture de données

In [12]:
import pandas as pd

La "magic" %ls permet de lister le contenu d'un répertoire (comme ls sour Linux, dir sous Windows)

In [13]:
%ls *.csv
log-20150128-092830.csv

Nom du fichier à lire (qui est ici dans le répertoire courant où le Notebook a été exécuté).

In [14]:
%pwd
Out[14]:
u'/home/pierre'
In [15]:
fname = 'log-20150128-092830.csv'

Affichage du contenu du fichier:

  • head et tail sous Linux (appel au shell avec le signe !)
  • sinon magic %more
In [16]:
!tail $fname








In [17]:
# affiche *tout* le fichier dans le pager
%more $fname

a) Lecture (parsing) du ficher CSV:

la fonction qui fait le travail: read_csv.

  • (+) Pratique,
  • (-) mais avec beaucoup d'options
In [18]:
# Afficher l'aide (syntaxe spécifique à la console IPython)
pd.read_csv?

Appel simple, en précisant juste le séparateur des champs:

In [20]:
d = pd.read_csv(fname, sep=';')
d.head()
Out[20]:
date time type value unit
0 28/01/2015 09:28:45.871 indoor relative humidity 36.00 %
1 28/01/2015 09:29:44.274 indoor relative humidity 36.00 %
2 28/01/2015 09:30:46.377 indoor relative humidity 37.00 %
3 28/01/2015 09:30:50.697 indoor temperature 21.20 �C
4 28/01/2015 09:30:55.908 volatile organic compounds 0.06 ppm

→ Lecture sans erreur, mais les dates ne sont pas analysées (elles restent sous forme de chaînes de caractères)

In [24]:
d['date'][0]
Out[24]:
'28/01/2015'
In [25]:
type(d['date'][0])
Out[25]:
str

Appel de read_csv avec des options adaptées à ce fichier:

In [26]:
d = pd.read_csv(fname, sep=';',
                parse_dates=[[0,1]], # aller chercher la date dans les 2 première colonnes
                index_col=0, # utiliser la date comme index des données
                dayfirst=True, # ne pas confondre le 5 février avec le 2 mai (format américain MM-DD-YYYY)
                )
d.head()
Out[26]:
type value unit
date_time
2015-01-28 09:28:45.871000 indoor relative humidity 36.00 %
2015-01-28 09:29:44.274000 indoor relative humidity 36.00 %
2015-01-28 09:30:46.377000 indoor relative humidity 37.00 %
2015-01-28 09:30:50.697000 indoor temperature 21.20 �C
2015-01-28 09:30:55.908000 volatile organic compounds 0.06 ppm

b) Manipulation des données

les donnés sont dans un pandas.DataFrame

In [27]:
type(d)
Out[27]:
pandas.core.frame.DataFrame

On récupère une colonne par indexation. Objet de type Series

In [29]:
vals = d['value']
vals.head()
Out[29]:
date_time
2015-01-28 09:28:45.871000    36.00
2015-01-28 09:29:44.274000    36.00
2015-01-28 09:30:46.377000    37.00
2015-01-28 09:30:50.697000    21.20
2015-01-28 09:30:55.908000     0.06
Name: value, dtype: float64
In [30]:
type(vals)
Out[30]:
pandas.core.series.Series

Tout les type de mesures issues du capteur

In [31]:
type_meas = d['type']
type_meas.unique()
Out[31]:
array(['indoor relative humidity', 'indoor temperature',
       'volatile organic compounds', 'carbon dioxide'], dtype=object)

Récupérer seulement un type de mesures: la température

In [35]:
temp = vals[d['type'] == 'indoor temperature']
temp.head(3)
Out[35]:
date_time
2015-01-28 09:30:50.697000    21.2
2015-01-28 09:33:49.494000    21.4
2015-01-28 09:36:51.682000    21.2
Name: value, dtype: float64

Exo: faire la même chose pour le CO2

In [43]:
co2 = vals[d['type'] == 'carbon dioxide']
co2.head(3)
Out[43]:
date_time
2015-01-28 09:31:01.408000    390
2015-01-28 09:33:59.015000    390
2015-01-28 09:37:01.512000    390
Name: value, dtype: float64

Statistique par groupes: groupby

Par type de mesure

In [172]:
d.groupby('type').mean()
Out[172]:
value
type
carbon dioxide 441.214809
indoor relative humidity 29.069816
indoor temperature 20.549659
volatile organic compounds 0.059591

Statistiques par jour

In [39]:
temp.groupby(temp.index.dayofyear).mean()
Out[39]:
28    21.091349
29    21.766250
30    21.304792
31    19.128750
32    18.576458
33    20.560417
34    21.048125
35    20.702917
36    21.225649
Name: value, dtype: float64

Changer la fréquence: rééchantillonage

Sous échantillonage: moyennes journalières

In [55]:
temp.resample('1D')
Out[55]:
date_time
2015-01-28    21.091349
2015-01-29    21.766250
2015-01-30    21.304792
2015-01-31    19.128750
2015-02-01    18.576458
2015-02-02    20.560417
2015-02-03    21.048125
2015-02-04    20.702917
2015-02-05    21.225649
Freq: D, Name: value, dtype: float64

Sur échantillonage: il faut combler les trous

In [62]:
temp.resample('1Min').head()
Out[62]:
date_time
2015-01-28 09:30:00    21.2
2015-01-28 09:31:00     NaN
2015-01-28 09:32:00     NaN
2015-01-28 09:33:00    21.4
2015-01-28 09:34:00     NaN
Freq: T, Name: value, dtype: float64
In [57]:
temp.resample('1Min', fill_method='bfill').head()
Out[57]:
date_time
2015-01-28 09:30:00    21.2
2015-01-28 09:31:00    21.4
2015-01-28 09:32:00    21.4
2015-01-28 09:33:00    21.4
2015-01-28 09:34:00    21.2
Freq: T, Name: value, dtype: float64
In [60]:
temp.resample('1Min').interpolate().head()
Out[60]:
date_time
2015-01-28 09:30:00    21.200000
2015-01-28 09:31:00    21.266667
2015-01-28 09:32:00    21.333333
2015-01-28 09:33:00    21.400000
2015-01-28 09:34:00    21.333333
Freq: T, Name: value, dtype: float64

c) Tracé des données capteur

In [64]:
%matplotlib inline
In [75]:
temp.plot()
plt.ylabel(u'température (°C)') # u'' pour des textes accentués
Out[75]:
<matplotlib.text.Text at 0x7f208c9ec410>

Deux tracés sur le même axe: twinx (pb de grille non superposée)

In [74]:
temp.plot()
plt.twinx()
co2.plot(style='r');

La température et le CO2, superposés: subplots

In [71]:
fig, (ax1, ax2) = plt.subplots(2,1, sharex=True)

temp.plot(ax=ax1)

co2.plot(ax=ax2, style='r')
plt.xlim('2015-02-03','2015-02-04')
ax2.set_ylim(200, 1200);

Remarque: grâce au paramètre sharex, le zoom est synchronisé (utile en mode interactif, avec %matplotlib qt).