Dans certaines situations où la structure des colonnes peut être inconnue, il peut être utile de se développer dans un MultiIndex pour effectuer des opérations qui nous permettent de tirer parti de l'indexation de pandas pour garantir l'intégrité des données lors de nos calculs.
En supposant une structure de yr[num]cd
associée à yr[num]qty
, nous pouvons isoler ces colonnes et créer un multi-index de telle sorte que les valeurs [num] soient à leur propre niveau.
v = df.filter(regex='^yr\d+(cd|qty)$')
v.columns = (
v.columns
.str.replace(r'yr(\d+)(cd|qty)', r'\2_\1', regex=True)
.str.split('_', expand=True)
)
Ici, j'ai isolé les colonnes intéressantes dans la variable v
et restructuré les colonnes de telle sorte que les valeurs cd
et qty
soient au niveau 0 et les chiffres au niveau 1 grâce à l'utilisation de replace et split.
V ressemble à:
cd qty cd qty cd qty
10 10 9 9 8 8
0 A 45 A 47 A 49
1 T 58 A 52 0 0
2 A 75 0 0 0 0
Remarquez qu'il existe de nombreuses façons de restructurer les colonnes en un MultiIndex. Voici un autre exemple à titre d'illustration:
v.columns = (
v.columns
.str.split(r'(\d+)', regex=True, expand=True)
.droplevel(0)
.swaplevel(0, 1)
)
En fonction du format du nom des colonnes, différentes approches peuvent être meilleures pour la restructuration.
Le principal avantage d'un MultiIndex avec cet ordre de niveaux est que nous pouvons très facilement accéder à toutes les colonnes cd
et qty
en y accédant v['cd']
et v['qty']
.
v['qty']
à titre d'illustration:
10 9 8
0 45 47 49
1 58 52 0
2 75 0 0
Le grand avantage ici est que, quel que soit l'ordre des colonnes, nous pouvons aligner de manière fiable les calculs entre 10, 9 et 8.
Cela nous permet de filtrer les valeurs égales à 'A' avec where is égal à 'A' v['qty'].where(v['cd'].eq('A'))
:
10 9 8
0 45.0 47.0 49.0
1 NaN 52.0 NaN
2 75.0 NaN NaN
Puis prendre la moyenne à travers les lignes v['qty'].where(v['cd'].eq('A')).mean(axis='columns')
:
0 47.0
1 52.0
2 75.0
dtype: float64
Cela a le même index que df
, nous pouvons donc très simplement assigner les valeurs de retour
df['Average'] = v['qty'].where(v['cd'].eq('A')).mean(axis='columns')
df
avec la nouvelle colonne:
Sales yr8cd yr9cd yr10cd yr10qty yr9qty yr8qty Average
0 42 A A A 45 47 49 47.0
1 56 0 A T 58 52 0 52.0
2 78 0 0 A 75 0 0 75.0
Encore une fois, l'aspect vraiment génial de cette approche est que l'ordre initial des colonnes de données n'a pas d'importance.
Imaginez une situation où nos colonnes cd
sont regroupées avec des nombres par ordre croissant et nos colonnes qty
sont regroupées avec des nombres par ordre décroissant.
Sales
yr8cd
yr9cd
yr10cd
yr10qty
yr9qty
yr8qty
42
A
A
A
45
47
49
56
0
A
T
58
52
0
78
0
0
A
75
0
0
Ou, de manière plus réaliste, imaginez un scénario où quelqu'un a accidentellement fait glisser l'une des colonnes hors de l'ordre. L'approche décrite ici donnera toujours les bonnes valeurs moyennes car le calcul et le filtrage des valeurs sont alignés en fonction des valeurs numériques et non de leur position relative dans le DataFrame.
0 47.0
1 52.0
2 75.0
dtype: float64
Ainsi, bien que cette approche ne soit probablement pas la solution la plus rapide ou la plus efficiente en termes de mémoire, elle est assez performante sans sacrifier les vérifications d'intégrité d'alignement que l'indexation peut offrir.
Exemple complet avec numéro de version
import pandas as pd # v2.1.2
df = pd.DataFrame({
'Sales': [42, 56, 78],
'yr10cd': ['A', 'T', 'A'],
'yr10qty': [45, 58, 75],
'yr9cd': ['A', 'A', '0'],
'yr9qty': [47, 52, 0],
'yr8cd': ['A', '0', '0'],
'yr8qty': [49, 0, 0]
})
v = df.filter(regex='^yr\d+(cd|qty)$')
v.columns = (
v.columns
.str.replace(r'yr(\d+)(cd|qty)', r'\2_\1', regex=True)
.str.split('_', expand=True)
)
df['Average'] = v['qty'].where(v['cd'].eq('A')).mean(axis='columns')
print(df)