Cube de données et pandas

pandas

[1]:
%matplotlib inline

Un cube

Les données sont disposées sous forme de table, il y a N colonnes de coordonnées et une colonne numérique. Avec N=4, cela équivaut à une fonction f(x_1,x_2,x_3,x_4)=y.

[3]:
import pandas

data = [
    {"X1": "A1", "X2": "B1", "X3": "C1", "X4": "D1", "Y": 3},
    {"X1": "A1", "X2": "B1", "X3": "C1", "X4": "D2", "Y": 4},
    {"X1": "A1", "X2": "B1", "X3": "C2", "X4": "D1", "Y": 5},
    {"X1": "A1", "X2": "B1", "X3": "C2", "X4": "D2", "Y": 6},
    {"X1": "A1", "X2": "B2", "X3": "C1", "X4": "D1", "Y": 7},
    {"X1": "A1", "X2": "B2", "X3": "C1", "X4": "D2", "Y": 8},
    {"X1": "A1", "X2": "B2", "X3": "C2", "X4": "D1", "Y": 9},
    {"X1": "A1", "X2": "B2", "X3": "C2", "X4": "D2", "Y": 2},
    {"X1": "A2", "X2": "B2", "X3": "C2", "X4": "D2", "Y": 1},
]
df = pandas.DataFrame(data)
df
[3]:
X1 X2 X3 X4 Y
0 A1 B1 C1 D1 3
1 A1 B1 C1 D2 4
2 A1 B1 C2 D1 5
3 A1 B1 C2 D2 6
4 A1 B2 C1 D1 7
5 A1 B2 C1 D2 8
6 A1 B2 C2 D1 9
7 A1 B2 C2 D2 2
8 A2 B2 C2 D2 1

Pivot

Le pivot consiste à choisir des colonnes pour les indices des lignes, et d’autres pour les colonnes. Le pivot fonctionne si chaque valeur numérique peut être identifiée de manière unique.

[4]:
df.pivot(index=["X1", "X2"], columns=["X3", "X4"], values="Y")
[4]:
X3 C1 C2
X4 D1 D2 D1 D2
X1 X2
A1 B1 3.0 4.0 5.0 6.0
B2 7.0 8.0 9.0 2.0
A2 B2 NaN NaN NaN 1.0
[5]:
df.pivot(index=["X1"], columns=["X2", "X3", "X4"], values="Y")
[5]:
X2 B1 B2
X3 C1 C2 C1 C2
X4 D1 D2 D1 D2 D1 D2 D1 D2
X1
A1 3.0 4.0 5.0 6.0 7.0 8.0 9.0 2.0
A2 NaN NaN NaN NaN NaN NaN NaN 1.0

Index et MultiIndex

A quoi correspondent les index ?

[6]:
piv = df.pivot(index=["X1"], columns=["X2", "X3", "X4"], values="Y")
piv.columns
[6]:
MultiIndex([('B1', 'C1', 'D1'),
            ('B1', 'C1', 'D2'),
            ('B1', 'C2', 'D1'),
            ('B1', 'C2', 'D2'),
            ('B2', 'C1', 'D1'),
            ('B2', 'C1', 'D2'),
            ('B2', 'C2', 'D1'),
            ('B2', 'C2', 'D2')],
           names=['X2', 'X3', 'X4'])
[12]:
piv.index
[12]:
Index(['A1', 'A2'], dtype='object', name='X1')
[14]:
piv = df.pivot(index=["X1", "X2"], columns=["X3", "X4"], values="Y")
piv.columns
[14]:
MultiIndex([('C1', 'D1'),
            ('C1', 'D2'),
            ('C2', 'D1'),
            ('C2', 'D2')],
           names=['X3', 'X4'])
[15]:
piv.index
[15]:
MultiIndex([('A1', 'B1'),
            ('A1', 'B2'),
            ('A2', 'B2')],
           names=['X1', 'X2'])

Les Index indexent les valeurs et sont équivalents à des colonnes, les MultiIndex indexent les valeurs et sont équivalents à plusieurs colonnes. On récupère le nom des colonnes avec la propriété names.

[16]:
piv.columns.names
[16]:
FrozenList(['X3', 'X4'])
[17]:
piv.index.names
[17]:
FrozenList(['X1', 'X2'])

On récupère le nombre de colonnes avec nlevels.

[20]:
piv.columns.nlevels, piv.index.nlevels
[20]:
(2, 2)

Ou encore levels.

[19]:
piv.columns.levels, piv.index.levels
[19]:
(FrozenList([['C1', 'C2'], ['D1', 'D2']]),
 FrozenList([['A1', 'A2'], ['B1', 'B2']]))

On veut accéder à un élément.

[21]:
piv.loc["A1"]
[21]:
X3 C1 C2
X4 D1 D2 D1 D2
X2
B1 3.0 4.0 5.0 6.0
B2 7.0 8.0 9.0 2.0
[24]:
piv.loc["A1", "B1"]
[24]:
X3  X4
C1  D1    3.0
    D2    4.0
C2  D1    5.0
    D2    6.0
Name: (A1, B1), dtype: float64
[25]:
piv["C1"]
[25]:
X4 D1 D2
X1 X2
A1 B1 3.0 4.0
B2 7.0 8.0
A2 B2 NaN NaN
[26]:
piv["C1", "D1"]
[26]:
X1  X2
A1  B1    3.0
    B2    7.0
A2  B2    NaN
Name: (C1, D1), dtype: float64

Passer de l’un à l’autre

Peut-on retrouver les données originales à partir du pivot?

[27]:
piv
[27]:
X3 C1 C2
X4 D1 D2 D1 D2
X1 X2
A1 B1 3.0 4.0 5.0 6.0
B2 7.0 8.0 9.0 2.0
A2 B2 NaN NaN NaN 1.0

Ce qui ne marche pas reset_index.

[28]:
piv.reset_index(drop=False)
[28]:
X3 X1 X2 C1 C2
X4 D1 D2 D1 D2
0 A1 B1 3.0 4.0 5.0 6.0
1 A1 B2 7.0 8.0 9.0 2.0
2 A2 B2 NaN NaN NaN 1.0

Ce qui marche…

  • stack : fait passer une coordonnée des colonnes aux lignes

  • unstack : fait passer une coordonnée des lignes aux colonnes

[31]:
piv.stack(0, future_stack=True)
[31]:
X4 D1 D2
X1 X2 X3
A1 B1 C1 3.0 4.0
C2 5.0 6.0
B2 C1 7.0 8.0
C2 9.0 2.0
A2 B2 C1 NaN NaN
C2 NaN 1.0
[32]:
piv.stack([0, 1], future_stack=True)
[32]:
X1  X2  X3  X4
A1  B1  C1  D1    3.0
            D2    4.0
        C2  D1    5.0
            D2    6.0
    B2  C1  D1    7.0
            D2    8.0
        C2  D1    9.0
            D2    2.0
A2  B2  C1  D1    NaN
            D2    NaN
        C2  D1    NaN
            D2    1.0
dtype: float64
[33]:
piv.stack("X4", future_stack=True)
[33]:
X3 C1 C2
X1 X2 X4
A1 B1 D1 3.0 5.0
D2 4.0 6.0
B2 D1 7.0 9.0
D2 8.0 2.0
A2 B2 D1 NaN NaN
D2 NaN 1.0
[34]:
piv.unstack("X1")
[34]:
X3 C1 C2
X4 D1 D2 D1 D2
X1 A1 A2 A1 A2 A1 A2 A1 A2
X2
B1 3.0 NaN 4.0 NaN 5.0 NaN 6.0 NaN
B2 7.0 NaN 8.0 NaN 9.0 NaN 2.0 1.0

On peut changer l’ordre des index.

[41]:
view = piv.unstack("X1")
new_index = view.columns.reorder_levels(["X1", "X3", "X4"])
new_index
[41]:
MultiIndex([('A1', 'C1', 'D1'),
            ('A2', 'C1', 'D1'),
            ('A1', 'C1', 'D2'),
            ('A2', 'C1', 'D2'),
            ('A1', 'C2', 'D1'),
            ('A2', 'C2', 'D1'),
            ('A1', 'C2', 'D2'),
            ('A2', 'C2', 'D2')],
           names=['X1', 'X3', 'X4'])
[43]:
view.columns = new_index
view
[43]:
X1 A1 A2 A1 A2 A1 A2 A1 A2
X3 C1 C1 C1 C1 C2 C2 C2 C2
X4 D1 D1 D2 D2 D1 D1 D2 D2
X2
B1 3.0 NaN 4.0 NaN 5.0 NaN 6.0 NaN
B2 7.0 NaN 8.0 NaN 9.0 NaN 2.0 1.0

Un pivot aggrégé

La fonction pivot suppose que la transformation conserve chaque valeur sans les aggréger ce qui permet de restaurer les données sous leurs forme initiale. Mais ce n’est pas toujours ce qu’on souhaite faire.

[44]:
df
[44]:
X1 X2 X3 X4 Y
0 A1 B1 C1 D1 3
1 A1 B1 C1 D2 4
2 A1 B1 C2 D1 5
3 A1 B1 C2 D2 6
4 A1 B2 C1 D1 7
5 A1 B2 C1 D2 8
6 A1 B2 C2 D1 9
7 A1 B2 C2 D2 2
8 A2 B2 C2 D2 1
[46]:
try:
    df.pivot(index=["X2"], columns=["X3", "X4"], values="Y")
except Exception as e:
    print("Le pivot ne conserve pas les données.")
    print(e)
Le pivot ne conserve pas les données.
Index contains duplicate entries, cannot reshape

On utlise alors pivot_table.

[48]:
df.pivot_table(index=["X2"], columns=["X3", "X4"], values="Y", aggfunc="count")
[48]:
X3 C1 C2
X4 D1 D2 D1 D2
X2
B1 1 1 1 1
B2 1 1 1 2
[49]:
df.pivot_table(index=["X2"], columns=["X3", "X4"], values="Y", aggfunc="sum")
[49]:
X3 C1 C2
X4 D1 D2 D1 D2
X2
B1 3 4 5 6
B2 7 8 9 3

XArray

Le package XArray représente des cubes de données de façon plus efficace mais parfois moins intuitive.

[64]:
import xarray as xr

cube = xr.Dataset.from_dataframe(df.set_index(["X1", "X2", "X3", "X4"]))
cube
[64]:
<xarray.Dataset> Size: 192B
Dimensions:  (X1: 2, X2: 2, X3: 2, X4: 2)
Coordinates:
  * X1       (X1) object 16B 'A1' 'A2'
  * X2       (X2) object 16B 'B1' 'B2'
  * X3       (X3) object 16B 'C1' 'C2'
  * X4       (X4) object 16B 'D1' 'D2'
Data variables:
    Y        (X1, X2, X3, X4) float64 128B 3.0 4.0 5.0 6.0 ... nan nan nan 1.0

Modifier une valeur existante.

[65]:
cube["Y"].loc[{"X1": "A1", "X2": "B2", "X3": "C2", "X4": "D1"}] = 100
cube
[65]:
<xarray.Dataset> Size: 192B
Dimensions:  (X1: 2, X2: 2, X3: 2, X4: 2)
Coordinates:
  * X1       (X1) object 16B 'A1' 'A2'
  * X2       (X2) object 16B 'B1' 'B2'
  * X3       (X3) object 16B 'C1' 'C2'
  * X4       (X4) object 16B 'D1' 'D2'
Data variables:
    Y        (X1, X2, X3, X4) float64 128B 3.0 4.0 5.0 6.0 ... nan nan nan 1.0
[66]:
cube.mean(dim=("X2", "X3"))
[66]:
<xarray.Dataset> Size: 64B
Dimensions:  (X1: 2, X4: 2)
Coordinates:
  * X1       (X1) object 16B 'A1' 'A2'
  * X4       (X4) object 16B 'D1' 'D2'
Data variables:
    Y        (X1, X4) float64 32B 28.75 5.0 nan 1.0
[67]:
cube.to_dataframe()
[67]:
Y
X1 X2 X3 X4
A1 B1 C1 D1 3.0
D2 4.0
C2 D1 5.0
D2 6.0
B2 C1 D1 7.0
D2 8.0
C2 D1 100.0
D2 2.0
A2 B1 C1 D1 NaN
D2 NaN
C2 D1 NaN
D2 NaN
B2 C1 D1 NaN
D2 NaN
C2 D1 NaN
D2 1.0
[ ]:


Notebook on github