Complément - niveau intermédiaire¶
Création d’une DataFrame¶
Une DataFrame est un tableau numpy à deux dimensions avec un index pour les lignes et un index pour les colonnes. Il y a de nombreuses manières de construire une DataFrame.
# Regardons la construction d'une DataFrame
import numpy as np
import pandas as pd
# Créons une Series pour définir des âges
age = pd.Series([30, 20, 50], index=['alice', 'bob', 'julie'])
# et une Series pour définir des tailles
height = pd.Series([150, 170, 168], index=['alice', 'marc', 'julie'])
# On peut maintenant combiner ces deux Series en DataFrame,
# chaque Series définissant une colonne, une manière de le faire est
# de définir un dictionnaire qui contient pour clef le nom de la colonne
# et pour valeur la Series correspondante
stat = pd.DataFrame({'age': age, 'height': height})
print(stat) age height
alice 30.0 150.0
bob 20.0 NaN
julie 50.0 168.0
marc NaN 170.0
On remarque que pandas fait automatiquement l’alignement des index, lorsqu’une valeur n’est pas présente, elle est automatiquement remplacée par NaN. Panda va également broadcaster une valeur unique définissant une colonne sur toutes les lignes. Regardons cela :
stat = pd.DataFrame({'age': age, 'height': height, 'city': 'Nice'})
print(stat) age height city
alice 30.0 150.0 Nice
bob 20.0 NaN Nice
julie 50.0 168.0 Nice
marc NaN 170.0 Nice
# On peut maintenant accéder aux index des lignes et des colonnes
# l'index des lignes
print(stat.index)Index(['alice', 'bob', 'julie', 'marc'], dtype='str')
# l'index des colonnes
print(stat.columns)Index(['age', 'height', 'city'], dtype='str')
Il y a de nombreuses manières d’accéder aux éléments de la DataFrame, certaines sont bonnes et d’autres à proscrire, commençons par prendre de bonnes habitudes. Comme il s’agit d’une structure à deux dimensions, il faut donner un indice de ligne et de colonne :
# Quel est l'âge de alice
a = stat.loc['alice', 'age']# a est un flottant
type(a), a(numpy.float64, np.float64(30.0))# Quel est la moyenne de tous les âges
c = stat.loc[:, 'age']
m = c.mean()
print(f"L'âge moyen est de {m:.1f} ans.")L'âge moyen est de 33.3 ans.
# c est une Series
type(c)pandas.Series# et m est un flottant
type(m)numpy.float64On peut déjà noter plusieurs choses intéressantes :
On peut utiliser
.loc[]et.iloccomme pour lesSeries. Pour lesDataFramec’est encore plus important parce qu’il y a plus de risques d’ambiguïtés (notamment entre les lignes et les colonnes, on y reviendra) ;la méthode
meancalcule la moyenne, ça n’est pas surprenant, mais ignore lesNaN. C’est en général ce que l’on veut. Si vous vous demandez comment savoir si la méthode que vous utilisez ignore ou pas lesNaN, le mieux est de regarder l’aide de cette méthode. Il existe pour un certain nombre de méthodes deux versions : une qui ignore lesNaNet une autre qui les prend en compte ; on en reparlera.
Une autre manière de construire une DataFrame est de partir d’un array de numpy, et de spécifier les index pour les lignes et les colonnes avec les arguments index et columns :
a = np.random.randint(1, 20, 9).reshape(3, 3)
p = pd.DataFrame(a, index=['a', 'b', 'c'], columns=['x', 'y', 'z'])
print(p) x y z
a 19 17 4
b 2 13 6
c 14 5 9
Importation et exportation de données¶
En pratique, il est très fréquent que les données qu’on manipule soient stockées dans un fichier ou une base de données. Il existe en pandas de nombreux utilitaires pour importer et exporter des données et les convertir automatiquement en DataFrame. Vous pouvez importer ou exporter du CSV, JSON, HTML, Excel, HDF5, SQL, Python pickle, etc.
À titre d’illustration écrivons la DataFrame p dans différents formats.
# écrivons notre DataFrame dans un fichier CSV
p.to_csv('my_data.csv')
!cat my_data.csv,x,y,z
a,19,17,4
b,2,13,6
c,14,5,9
# et dans un fichier JSON
p.to_json('my_data.json')
!cat my_data.json{"x":{"a":19,"b":2,"c":14},"y":{"a":17,"b":13,"c":5},"z":{"a":4,"b":6,"c":9}}# on peut maintenant recharger notre fichier
# la conversion en DataFrame est automatique
new_p = pd.read_json('my_data.json')
print(new_p) x y z
a 19 17 4
b 2 13 6
c 14 5 9
Pour la gestion des autres formats, comme il s’agit de quelque chose de très spécifique et sans difficulté particulière, je vous renvoie simplement à la documentation :
http://
Manipulation d’une DataFrame¶
# construisons maintenant une DataFrame jouet
# voici une liste de prénoms
names = ['alice', 'bob', 'marc', 'bill', 'sonia']
# créons trois Series qui formeront les trois colonnes
age = pd.Series([12, 13, 16, 11, 16], index=names)
height = pd.Series([130, 140, 176, 120, 165], index=names)
sex = pd.Series(list('fmmmf'), index=names)
# créons maintenant la DataFrame
p = pd.DataFrame({'age': age, 'height': height, 'sex': sex})
print(p) age height sex
alice 12 130 f
bob 13 140 m
marc 16 176 m
bill 11 120 m
sonia 16 165 f
# et chargeons le jeux de données sur les pourboires de seaborn
import seaborn as sns
tips = sns.load_dataset('tips')pandas offre de nombreuses possibilités d’explorer les données. Attention, dans mes exemples je vais alterner entre le DataFrame p et le DataFrame tips suivant les besoins de l’explication.
# afficher les premières lignes
tips.head()# et les dernière lignes
tips.tail()# l'index des lignes
p.indexIndex(['alice', 'bob', 'marc', 'bill', 'sonia'], dtype='str')# et l'index des colonnes
p.columnsIndex(['age', 'height', 'sex'], dtype='str')# et afficher uniquement les valeurs
p.valuesarray([[12, 130, 'f'],
[13, 140, 'm'],
[16, 176, 'm'],
[11, 120, 'm'],
[16, 165, 'f']], dtype=object)# échanger lignes et colonnes
# cf. la transposition de matrices
p.TPour finir, il y a la méthodes describe qui permet d’obtenir des premières statistiques sur un DataFrame. describe permet de calculer des statistiques sur des type numériques, mais aussi sur des types chaînes de caractères.
# par défaut describe ne prend en compte que les colonnes numériques
p.describe()# mais on peut le forcer à prendre en compte toutes les colonnes
p.describe(include='all')Requêtes sur une DataFrame¶
On peut maintenant commencer à faire des requêtes sur les DataFrames. Les DataFrame supportent la notion de masque que l’on a vue pour les ndarray de numpy et pour les Series.
# p.loc prend soit un label de ligne
print(p.loc['sonia'])age 16
height 165
sex f
Name: sonia, dtype: object
# ou alors un label de ligne ET de colonne
print(p.loc['sonia', 'age'])16
On peut mettre à la place d’une label :
une liste de labels ;
un slice sur les labels ;
un masque (c’est-à-dire un tableau de booléens) ;
un callable qui retourne une des trois premières possibilités.
Noter que l’on peut également utiliser la notation .iloc[] avec les mêmes règles, mais elle est moins utile.
Je recommande de toujours utiliser la notation .loc[lignes, colonnes] pour éviter toute ambiguïté. Nous verrons que les notations .loc[lignes] ou pire seulement [label] sont sources d’erreurs.
Regardons maintenant d’autres exemples plus sophistiqués :
# un masque sur les femmes
p.loc[:, 'sex'] == 'f'alice True
bob False
marc False
bill False
sonia True
Name: sex, dtype: bool# si bien que pour construire un tableau
# avec uniquement les femmes
p.loc[p.loc[:, 'sex'] == 'f', :]# si on veut ne garder uniquement
# que les femmes de plus de 14 ans
p.loc[(p.loc[:, 'sex'] == 'f') & (p.loc[:, 'age'] > 14), :]# quelle est la moyenne de 'total_bill' pour les femmes
addition_f = tips.loc[tips.loc[:, 'sex'] == 'Female', 'total_bill'].mean()
print(f"addition moyenne des femmes : {addition_f:.2f}")addition moyenne des femmes : 18.06
# quelle est la note moyenne des hommes
addition_h = tips.loc[tips.loc[:, 'sex'] == 'Male', 'total_bill'].mean()
print(f"addition moyenne des hommes : {addition_h:.2f}")addition moyenne des hommes : 20.74
# qui laisse le plus grand pourcentage de pourboire :
# les hommes ou les femmes ?
pourboire_f = tips.loc[tips.loc[:, 'sex'] == 'Female', 'tip'].mean()
pourboire_h = tips.loc[tips.loc[:, 'sex'] == 'Male', 'tip'].mean()
print(f"Les femmes laissent {pourboire_f/addition_f:.2%} de pourboire")
print(f"Les hommes laissent {pourboire_h/addition_h:.2%} de pourboire")Les femmes laissent 15.69% de pourboire
Les hommes laissent 14.89% de pourboire
Erreurs fréquentes et ambiguïtés sur les requêtes¶
Nous avons vu une manière simple et non ambiguë de faire des requêtes sur les DataFrame. Nous allons voir qu’il existe d’autres manières qui ont pour seul avantage d’être plus concises, mais sources de nombreuses erreurs.
Souvenez-vous, utilisez toujours la notation .loc[lignes, colonnes] sinon, soyez sûr de savoir ce qui est réellement calculé.
# commençons par la notation la plus classique
p['sex'] # prend forcément un label de colonnealice f
bob m
marc m
bill m
sonia f
Name: sex, dtype: str# mais par contre, si on passe un slice, c'est forcément des lignes,
# assez perturbant et source de confusion.
p['alice': 'marc']# on peut même directement accéder à une colonne par son nom
p.agealice 12
bob 13
marc 16
bill 11
sonia 16
Name: age, dtype: int64Mais c’est fortement déconseillé parce que si un attribut de même nom existe sur une DataFrame, alors la priorité est donnée à l’attribut, et non à la colonne :
# ajoutons une colonne qui a pour nom une méthode qui existe sur
# les DataFrame
p['mean'] = 1
print(p) age height sex mean
alice 12 130 f 1
bob 13 140 m 1
marc 16 176 m 1
bill 11 120 m 1
sonia 16 165 f 1
# je peux bien accéder
# à la colonne sex
p.sexalice f
bob m
marc m
bill m
sonia f
Name: sex, dtype: str# mais pas à la colonne mean
p.mean<bound method DataFrame.mean of age height sex mean
alice 12 130 f 1
bob 13 140 m 1
marc 16 176 m 1
bill 11 120 m 1
sonia 16 165 f 1># à nouveau, la seule méthode non ambiguë est d'utiliser .loc
p.loc[:, 'mean']alice 1
bob 1
marc 1
bill 1
sonia 1
Name: mean, dtype: int64# supprimons maintenant la colonne mean *en place* (par défaut,
# drop retourne une nouvelle DataFrame)
p.drop(columns='mean', inplace=True)
print(p) age height sex
alice 12 130 f
bob 13 140 m
marc 16 176 m
bill 11 120 m
sonia 16 165 f
Pour aller plus loin, vous pouvez lire la documentation officielle :
http://
Universal functions et pandas¶
Ça n’est pas une surprise, les Series et DataFrame de pandas supportent les ufunc de numpy. Mais il y a une subtilité. Il est parfaitement légitime et correct d’appliquer une ufunc de numpy sur les éléments d’une DataFrame :
d = pd.DataFrame(np.random.randint(
1, 10, 9).reshape(3, 3), columns=list('abc'))
print(d) a b c
0 9 5 4
1 7 6 2
2 8 2 3
np.log(d)Nous remarquons que comme on s’y attend, la ufunc a été appliquée à chaque élément de la DataFrame et que les labels des lignes et colonnes ont été préservés.
Par contre, si l’on a besoin d’alignement de labels, c’est le cas avec toutes les opérations qui s’appliquent sur deux objets comme une addition, alors les ufunc de numpy ne vont pas faire ce à quoi on s’attend. Elles vont faire les opérations sur les tableaux numpy sans prendre en compte les labels.
Pour avoir un alignement des labels, il faut utiliser les ufunc de pandas.
# prenons deux Series
s1 = pd.Series([10, 20, 30],
index=list('abc'))
print(s1)a 10
b 20
c 30
dtype: int64
#
s2 = pd.Series([12, 22, 32],
index=list('acd'))
print(s2)a 12
c 22
d 32
dtype: int64
# la ufunc numpy fait la somme
# des arrays sans prendre en compte
# les labels, donc sans alignement
np.add(s1, s2)a 22.0
b NaN
c 52.0
d NaN
dtype: float64# la ufunc pandas va faire
# un alignement des labels
# cet appel est équivalent à s1 + s2
s1.add(s2)a 22.0
b NaN
c 52.0
d NaN
dtype: float64# comme on l'a vu sur le complément précédent, les valeurs absentes sont
# remplacées par NaN, mais on peut changer ce comportement lors de
# l'appel de .add
s1.add(s2, fill_value=0)a 22.0
b 20.0
c 52.0
d 32.0
dtype: float64# regardons un autre exemple sur des DataFrame
# on affiche tout ça dans les cellules suivantes
names = ['alice', 'bob', 'charle']
bananas = pd.Series([10, 3, 9], index=names)
oranges = pd.Series([3, 11, 6], index=names)
fruits_jan = pd.DataFrame({'bananas': bananas, 'orange': oranges})
bananas = pd.Series([6, 1], index=names[:-1])
apples = pd.Series([8, 5], index=names[1:])
fruits_feb = pd.DataFrame({'bananas': bananas, 'apples': apples})# ce qui donne
fruits_jan# et
fruits_feb# regardons maintenant la somme des fruits mangés
eaten_fruits = fruits_jan + fruits_feb
print(eaten_fruits) apples bananas orange
alice NaN 16.0 NaN
bob NaN 4.0 NaN
charle NaN NaN NaN
# On a bien un alignement des labels, mais il y a beaucoup de valeurs
# manquantes. Corrigeons cela on remplaçant les valeurs manquantes par 0
eaten_fruits = fruits_jan.add(fruits_feb, fill_value=0)
print(eaten_fruits) apples bananas orange
alice NaN 16.0 3.0
bob 8.0 4.0 11.0
charle 5.0 9.0 6.0
Notons que lorsqu’une valeur est absente dans toutes les DataFrame, NaN est conservé.
Un dernière subtilité à connaître lors de l’alignement des labels intervient lorsque vous faites une opération sur une DataFrame et une Series. pandas va considérer la Series comme une ligne et va la broadcaster sur les autres lignes. Par conséquent, l’index de la Series va être considéré comme des colonnes et aligné avec les colonnes de la DataFrame.
dataframe = pd.DataFrame(
np.random.randint(1, 10, size=(3, 3)),
columns=list('abc'), index=list('xyz'))
dataframeseries_row = pd.Series(
[100, 200, 300],
index=list('abc'))
series_rowa 100
b 200
c 300
dtype: int64series_col = pd.Series(
[400, 500, 600],
index=list('xyz'))
series_colx 400
y 500
z 600
dtype: int64# la Series est considérée comme une ligne et son index
# s'aligne sur les colonnes de la DataFrame
# la Series va être broadcastée
# sur les autres lignes de la DataFrame
dataframe + series_row# du coup si les labels ne correspondent pas,
# le résultat sera le suivant
dataframe + series_col# on peut dans ce cas, changer le comportement par défaut en forçant
# l'alignement de la Series suivant un autre axe avec l'argument axis
dataframe.add(series_col, axis=0)Ici, axis=0 signifie que la Series est considérée comme une colonne est qu’elle va être broadcastée sur les autres colonnes (le long de l’axe de ligne).
Opérations sur les chaînes de caractères¶
Nous allons maintenant parler de la vectorisation des opérations sur les chaînes de caractères. Il y a plusieurs choses importantes à savoir :
les méthodes sur les chaînes de caractères ne sont disponibles que pour les
Serieset lesIndex, mais pas pour lesDataFrame;ces méthodes ignorent les
NaNet remplacent les valeurs qui ne sont pas des chaînes de caractères parNaN;ces méthodes retournent une copie de l’objet (
SeriesouIndex), il n’y a pas de modification en place ;la plupart des méthodes Python sur le type
strexiste sous forme vectorisée ;on accède à ces méthodes avec la syntaxe :
Series.str.<vectorized method name>Index.str.<vectorized method name>
Regardons quelques exemples :
# Créons une Series avec des noms ayant une capitalisation inconsistante
# et une mauvaise gestion des espaces
names = ['alice ', ' bOB', 'Marc', 'bill', 3, ' JULIE ', np.nan]
age = pd.Series(names)# nettoyons maintenant ces données
# on met en minuscule
a = age.str.lower()
# on enlève les espaces
a = a.str.strip()
a0 alice
1 bob
2 marc
3 bill
4 NaN
5 julie
6 NaN
dtype: object# comme les méthodes vectorisées retournent un objet de même type, on
# peut les chaîner comme ceci
[x for x in age.str.lower().str.strip()]['alice', 'bob', 'marc', 'bill', nan, 'julie', nan]On peut également utiliser l’indexation des str de manière vectorisée :
print(a)0 alice
1 bob
2 marc
3 bill
4 NaN
5 julie
6 NaN
dtype: object
print(a.str[-1])0 e
1 b
2 c
3 l
4 NaN
5 e
6 NaN
dtype: object
Pour aller plus loin vous pouvez lire la documentation officielle :
http://
Gestion des valeurs manquantes¶
Nous avons vu que des opérations sur les DataFrame pouvaient générer des valeurs NaN lors de l’alignement. Il est également possible d’avoir de telles valeurs manquantes dans votre jeu de données original. pandas offre plusieurs possibilités pour gérer correctement ces valeurs manquantes.
Avant de voir ces différentes possibilités, définissons cette notion de valeur manquante.
Une valeur manquante peut-être représentée avec pandas soit par np.nan soit par l’objet Python None.
np.nanest un objet de typefloat, par conséquent il ne peut apparaître que dans un array defloatou un array d’object. Notons quenp.nanapparaît avecpandascomme simplementNaNet que dans la suite on utilise de manière indifférente les deux notations, par contre, dans du code, il faut obligatoirement utilisernp.nan;si on ajoute un
NaNdans un array d’entier, ils seront convertis enfloat64;si on ajoute un
NaNdans un array de booléens, ils seront convertis enobject;
NaNest contaminant, toute opération avec unNaNa pour résultatNaN;lorsque l’on utilise
None, il est automatiquement converti enNaNlorsque le type de l’array est numérique.
Illustrons ces propriétés :
# une Series d'entiers
s = pd.Series([1, 2])
s0 1
1 2
dtype: int64# on insère un NaN, la Series est alors convertie en float64
s[0] = np.nan
s0 NaN
1 2.0
dtype: float64# on réinitialise
s = pd.Series([1, 2])
s0 1
1 2
dtype: int64# et on insère None
s[0] = None
# Le résultat est le même
# None est converti en NaN
s0 NaN
1 2.0
dtype: float64Regardons maintenant, les méthodes de pandas pour gérer les valeurs manquantes (donc NaN ou None) :
isna()retourne un masque mettant àTrueles valeurs manquantes (il y a un aliasisnull()) ;notna()retourne un masque mettant àFalseles valeurs manquantes (il y a un aliasnotnull()) ;dropna()retourne un nouvel objet sans les valeurs manquantes ;fillna()retourne un nouvel objet avec les valeurs manquantes remplacées.
On remarque que l’ajout d’alias pour les méthodes est de nouveau une source de confusion avec laquelle il faut vivre.
On remarque également qu’alors que isnull() et notnull() sont des méthodes simples, dropna() et fillna() impliquent l’utilisation de stratégies. Regardons cela :
# créons une DataFrame avec quelques valeurs manquantes
names = ['alice', 'bob', 'charles']
bananas = pd.Series([6, 1], index=names[:-1])
apples = pd.Series([8, 5], index=names[1:])
fruits_feb = pd.DataFrame({'bananas': bananas, 'apples': apples})
print(fruits_feb) bananas apples
alice 6.0 NaN
bob 1.0 8.0
charles NaN 5.0
fruits_feb.isna()fruits_feb.notna()Par défaut, dropna() va enlever toutes les lignes qui contiennent au moins une valeur manquante. Mais on peut changer ce comportement avec des arguments :
p = pd.DataFrame([[1, 2, np.nan], [3, np.nan, np.nan], [7, 5, np.nan]])
print(p) 0 1 2
0 1 2.0 NaN
1 3 NaN NaN
2 7 5.0 NaN
# comportement par défaut, j'enlève toutes les lignes avec au moins
# une valeur manquante; il ne reste rien !
p.dropna()# maintenant, je fais l'opération par colonne
p.dropna(axis=1)# je fais l'opération par colonne si toute la colonne est manquante
p.dropna(axis=1, how='all')# je fais l'opération par ligne si au moins 2 valeurs sont manquantes
p.dropna(thresh=2)Par défaut, fillna() remplace les valeurs manquantes avec un argument pas défaut. Mais on peut ici aussi changer ce comportement. Regardons cela :
print(p) 0 1 2
0 1 2.0 NaN
1 3 NaN NaN
2 7 5.0 NaN
# je remplace les valeurs manquantes par -1
p.fillna(-1)# je remplace les valeurs manquantes avec la valeur suivante sur la colonne
# bfill est pour back fill, c'est-à-dire remplace en arrière à partir des
# valeurs existantes
p.fillna(method='bfill')---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[77], line 4
1 # je remplace les valeurs manquantes avec la valeur suivante sur la colonne
2 # bfill est pour back fill, c'est-à-dire remplace en arrière à partir des
3 # valeurs existantes
----> 4 p.fillna(method='bfill')
TypeError: NDFrame.fillna() got an unexpected keyword argument 'method'# je remplace les valeurs manquantes avec la valeur précédente sur la ligne
# ffill est pour forward fill, remplace en avant à partir des valeurs
# existantes
p.fillna(method='ffill', axis=1)Regardez l’aide de ces méthodes pour aller plus loin.
p.dropna?p.fillna?Analyse statistique des données¶
Nous n’avons pas le temps de couvrir les possibilités d’analyse statistique de la suite data science de Python. pandas offre quelques possibilités basiques avec des calculs de moyennes, d’écarts types ou de covariances que l’on peut éventuellement appliquer par fenêtres à un jeux de données. Pour avoir plus de détails dessus vous pouvez consulter cette documentation :
http://
Dans la suite data science de Python, il a aussi des modules spécialisés dans l’analyse statistique comme :
ou des outils de calculs scientifiques plus génériques comme SciPy.
De nouveau, il s’agit d’outils appliqués à des domaines spécifiques et ils se basent tous sur le couple numpy/pandas.
Complément - niveau avancé¶
Les MultiIndex¶
pandas avait historiquement d’autres structures de données en plus des Series et des DataFrame permettant d’exprimer des dimensionnalités supérieures à 2, comme par exemple les Panel. Mais pour des raisons de maintenance du code et d’optimisation, les développeurs ont décidé de ne garder que les Series et les DataFrame. Alors, comment exprimer des données avec plus de deux dimensions ?
On utilise pour cela des MultiIndex. Un MultiIndex est un index qui peut être utilisé partout où l’on utilise un index (dans une Series, ou comme ligne ou colonne d’une DataFrame) et qui a pour caractéristique d’avoir plusieurs niveaux.
Comme tous types d’index, et parce qu’un MultiIndex est une sous classe d’Index, pandas va correctement aligner les Series et les DataFrame avec des MultiIndex.
Regardons tout de suite un exemple :
# construisons une DataFrame jouet
# voici une liste de prénoms
names = ['alice', 'bob', 'sonia']
# créons trois Series qui formeront trois colonnes
age = pd.Series([12, 13, 16], index=names)
height = pd.Series([130, 140, 165], index=names)
sex = pd.Series(list('fmf'), index=names)
# créons maintenant la DataFrame
p = pd.DataFrame({'age': age, 'height': height, 'sex': sex})
print(p)# unstack, en première approximation, permet de passer d'une DataFrame à
# une Series avec un MultiIndex
s = p.unstack()
print(s)# et voici donc l'index de cette Series
s.indexIl existe évidemment des moyens de créer directement un MultiIndex et ensuite de le définir comme index d’une Series ou comme index de ligne ou colonne d’une DataFrame :
# on peut créer un MultiIndex à partir d'une liste de liste
names = ['alice', 'alice', 'alice', 'bob', 'bob', 'bob']
age = [2014, 2015, 2016, 2014, 2015, 2016]
s_list = pd.Series([40, 42, 45, 38, 40, 40], index=[names, age])
print(s_list)# ou à partir d'un dictionnaire de tuples
s_tuple = pd.Series({('alice', 2014): 40,
('alice', 2015): 42,
('alice', 2016): 45,
('bob', 2014): 38,
('bob', 2015): 40,
('bob', 2016): 40})
print(s_tuple)# ou avec la méthode from_product()
name = ['alice', 'bob']
year = [2014, 2015, 2016]
i = pd.MultiIndex.from_product([name, year])
s = pd.Series([40, 42, 45, 38, 40, 40], index=i)
print(s)On peut même nommer les niveaux d’un MultiIndex.
name = ['alice', 'bob']
year = [2014, 2015, 2016]
i = pd.MultiIndex.from_product([name, year], names=['name', 'year'])
s = pd.Series([40, 42, 45, 38, 40, 40], index=i)
print(s)# on peut changer le nom des niveaux du MultiIndex
s.index.names = ['NAMES', 'YEARS']
print(s)Créons maintenant une DataFrame jouet avec des MultiIndex pour étudier comment accéder aux éléments de la DataFrame.
index = pd.MultiIndex.from_product([[2013, 2014],
[1, 2, 3]],
names=['year',
'visit'])
columns = pd.MultiIndex.from_product([['Bob', 'Sue'],
['avant', 'arrière']],
names=['client',
'pression'])
# on crée des pressions de pneus factices
data = 2 + np.random.rand(6, 4)
# on crée la DataFrame
mecanics_data = pd.DataFrame(data, index=index, columns=columns)
print(mecanics_data)Il y a plusieurs manières d’accéder aux éléments, mais une seule que l’on recommande :
utilisez la notation .loc[ligne, colonne], .iloc[ligne, colonne].
# pression en 2013 pour Bob
mecanics_data.loc[2013, 'Bob']# pour accéder aux sous niveaux du MultiIndex, on utilise des tuples
mecanics_data.loc[(2013, 2), ('Bob', 'avant')]Le slice sur le MultiIndex est un peu délicat. On peut utiliser la notation : si on veut slicer sur tous les éléments d’un MultiIndex, sans prendre en compte un niveau. Si on spécifie les niveaux, il faut utiliser un objet slice ou pd.IndexSlice :
# slice(None) signifie tous les éléments du niveau
print(mecanics_data.loc[slice((2013, 2), (2014, 1)), ('Sue', slice(None))])# on peut utiliser la notation : si on ne distingue par les niveaux
print(mecanics_data.loc[(slice(None), slice(1, 2)), :])# on peut aussi utiliser pd.IndexSlice pour slicer avec une notation
# un peu plus concise
idx = pd.IndexSlice
print(mecanics_data.loc[idx[:, 1:2], idx['Sue', :]])Pour aller plus loin, regardez la documentation des MultiIndex :
http://
Conclusion¶
La DataFrame est la structure de données la plus souple et la plus puissante de pandas. Nous avons vu comment créer des DataFrame et comment accéder aux éléments. Nous verrons dans le prochain complément les techniques permettant de faire des opérations complexes (et proches dans l’esprit de ce que l’on peut faire avec une base de données) comme les opérations de merge ou de groupby.