Colonne de listes Pandas, créez une ligne pour chaque élément de la liste

163

J'ai un dataframe où certaines cellules contiennent des listes de plusieurs valeurs. Plutôt que de stocker plusieurs valeurs dans une cellule, j'aimerais étendre le dataframe afin que chaque élément de la liste ait sa propre ligne (avec les mêmes valeurs dans toutes les autres colonnes). Donc si j'ai:

import pandas as pd
import numpy as np

df = pd.DataFrame(
    {'trial_num': [1, 2, 3, 1, 2, 3],
     'subject': [1, 1, 1, 2, 2, 2],
     'samples': [list(np.random.randn(3).round(2)) for i in range(6)]
    }
)

df
Out[10]: 
                 samples  subject  trial_num
0    [0.57, -0.83, 1.44]        1          1
1    [-0.01, 1.13, 0.36]        1          2
2   [1.18, -1.46, -0.94]        1          3
3  [-0.08, -4.22, -2.05]        2          1
4     [0.72, 0.79, 0.53]        2          2
5    [0.4, -0.32, -0.13]        2          3

Comment puis-je convertir en forme longue, par exemple:

   subject  trial_num  sample  sample_num
0        1          1    0.57           0
1        1          1   -0.83           1
2        1          1    1.44           2
3        1          2   -0.01           0
4        1          2    1.13           1
5        1          2    0.36           2
6        1          3    1.18           0
# etc.

L'index n'est pas important, vous pouvez définir les colonnes existantes comme index et le classement final n'est pas important.

Marius
la source
11
À partir de pandas 0.25, vous pouvez également utiliser df.explode('samples')pour résoudre ce problème. explodene peut supporter que l'explosion d'une colonne pour le moment.
cs95

Réponses:

48
lst_col = 'samples'

r = pd.DataFrame({
      col:np.repeat(df[col].values, df[lst_col].str.len())
      for col in df.columns.drop(lst_col)}
    ).assign(**{lst_col:np.concatenate(df[lst_col].values)})[df.columns]

Résultat:

In [103]: r
Out[103]:
    samples  subject  trial_num
0      0.10        1          1
1     -0.20        1          1
2      0.05        1          1
3      0.25        1          2
4      1.32        1          2
5     -0.17        1          2
6      0.64        1          3
7     -0.22        1          3
8     -0.71        1          3
9     -0.03        2          1
10    -0.65        2          1
11     0.76        2          1
12     1.77        2          2
13     0.89        2          2
14     0.65        2          2
15    -0.98        2          3
16     0.65        2          3
17    -0.30        2          3

PS ici, vous pouvez trouver une solution un peu plus générique


MISE À JOUR: quelques explications: IMO, le moyen le plus simple de comprendre ce code est d'essayer de l'exécuter étape par étape:

dans la ligne suivante, nous répétons les valeurs dans une colonne Nfois où N- est la longueur de la liste correspondante:

In [10]: np.repeat(df['trial_num'].values, df[lst_col].str.len())
Out[10]: array([1, 1, 1, 2, 2, 2, 3, 3, 3, 1, 1, 1, 2, 2, 2, 3, 3, 3], dtype=int64)

cela peut être généralisé pour toutes les colonnes, contenant des valeurs scalaires:

In [11]: pd.DataFrame({
    ...:           col:np.repeat(df[col].values, df[lst_col].str.len())
    ...:           for col in df.columns.drop(lst_col)}
    ...:         )
Out[11]:
    trial_num  subject
0           1        1
1           1        1
2           1        1
3           2        1
4           2        1
5           2        1
6           3        1
..        ...      ...
11          1        2
12          2        2
13          2        2
14          2        2
15          3        2
16          3        2
17          3        2

[18 rows x 2 columns]

en utilisant, np.concatenate()nous pouvons aplatir toutes les valeurs de la listcolonne ( samples) et obtenir un vecteur 1D:

In [12]: np.concatenate(df[lst_col].values)
Out[12]: array([-1.04, -0.58, -1.32,  0.82, -0.59, -0.34,  0.25,  2.09,  0.12,  0.83, -0.88,  0.68,  0.55, -0.56,  0.65, -0.04,  0.36, -0.31])

mettre tout cela ensemble:

In [13]: pd.DataFrame({
    ...:           col:np.repeat(df[col].values, df[lst_col].str.len())
    ...:           for col in df.columns.drop(lst_col)}
    ...:         ).assign(**{lst_col:np.concatenate(df[lst_col].values)})
Out[13]:
    trial_num  subject  samples
0           1        1    -1.04
1           1        1    -0.58
2           1        1    -1.32
3           2        1     0.82
4           2        1    -0.59
5           2        1    -0.34
6           3        1     0.25
..        ...      ...      ...
11          1        2     0.68
12          2        2     0.55
13          2        2    -0.56
14          2        2     0.65
15          3        2    -0.04
16          3        2     0.36
17          3        2    -0.31

[18 rows x 3 columns]

L'utilisation pd.DataFrame()[df.columns]garantit que nous sélectionnons les colonnes dans l'ordre d'origine ...

MaxU
la source
3
Cela devrait être la réponse acceptée. La réponse actuellement acceptée est beaucoup, beaucoup plus lente que celle-ci.
irene
1
Je ne peux pas comprendre comment résoudre ce problème: TypeError: Impossible de convertir les données du tableau de dtype ('float64') vers dtype ('int64') selon la règle 'safe'
Greg
1
C'est la seule réponse qui a fonctionné pour moi, sur les 10+ trouvées pendant une heure complète de recherche dans les piles. Merci MaxU 🙏
olisteadman
1
Notez que cela supprime lst_colentièrement les lignes qui ont une liste vide ; pour conserver ces lignes et les remplir lst_colavec np.nan, vous pouvez simplement le faire df[lst_col] = df[lst_col].apply(lambda x: x if len(x) > 0 else [np.nan])avant d'utiliser cette méthode. Évidemment .mask, ne renverra pas de listes, d'où le .apply.
Charles Davis
C'est une excellente réponse qui devrait être acceptée. Bien que ce soit une réponse de niveau de magie noire, et pour ma part, j'apprécierais quelques explications sur ce que ces étapes font en fait.
ifly6
129

Un peu plus longtemps que prévu:

>>> df
                samples  subject  trial_num
0  [-0.07, -2.9, -2.44]        1          1
1   [-1.52, -0.35, 0.1]        1          2
2  [-0.17, 0.57, -0.65]        1          3
3  [-0.82, -1.06, 0.47]        2          1
4   [0.79, 1.35, -0.09]        2          2
5   [1.17, 1.14, -1.79]        2          3
>>>
>>> s = df.apply(lambda x: pd.Series(x['samples']),axis=1).stack().reset_index(level=1, drop=True)
>>> s.name = 'sample'
>>>
>>> df.drop('samples', axis=1).join(s)
   subject  trial_num  sample
0        1          1   -0.07
0        1          1   -2.90
0        1          1   -2.44
1        1          2   -1.52
1        1          2   -0.35
1        1          2    0.10
2        1          3   -0.17
2        1          3    0.57
2        1          3   -0.65
3        2          1   -0.82
3        2          1   -1.06
3        2          1    0.47
4        2          2    0.79
4        2          2    1.35
4        2          2   -0.09
5        2          3    1.17
5        2          3    1.14
5        2          3   -1.79

Si vous voulez un index séquentiel, vous pouvez appliquer reset_index(drop=True)au résultat.

mise à jour :

>>> res = df.set_index(['subject', 'trial_num'])['samples'].apply(pd.Series).stack()
>>> res = res.reset_index()
>>> res.columns = ['subject','trial_num','sample_num','sample']
>>> res
    subject  trial_num  sample_num  sample
0         1          1           0    1.89
1         1          1           1   -2.92
2         1          1           2    0.34
3         1          2           0    0.85
4         1          2           1    0.24
5         1          2           2    0.72
6         1          3           0   -0.96
7         1          3           1   -2.72
8         1          3           2   -0.11
9         2          1           0   -1.33
10        2          1           1    3.13
11        2          1           2   -0.65
12        2          2           0    0.10
13        2          2           1    0.65
14        2          2           2    0.15
15        2          3           0    0.64
16        2          3           1   -0.10
17        2          3           2   -0.76
Roman Pekar
la source
Merci, même la première étape de l'application pour obtenir chaque élément dans sa propre colonne est d'une aide précieuse. J'ai pu trouver une façon légèrement différente de le faire, mais il reste encore quelques étapes à franchir. Apparemment, ce n'est pas simple à faire chez les Pandas!
Marius
1
Très bonne réponse. Vous pouvez le raccourcir un peu en le remplaçant df.apply(lambda x: pd.Series(x['samples']),axis=1)par df.samples.apply(pd.Series).
Dennis Golomazov
1
Note aux lecteurs: cela souffre terriblement de problèmes de performances. Voir ici pour une solution beaucoup plus performante utilisant numpy.
cs95
2
quelle est la solution lorsque le nombre d'échantillons n'est pas le même pour toutes les lignes?
SarahData
@SarahData Utilisez df.explode()comme indiqué ici.
cs95 le
64

Pandas> = 0,25

Les méthodes Series et DataFrame définissent une .explode()méthode qui décompose les listes en lignes séparées. Consultez la section de documentation sur l' explosion d'une colonne de type liste .

df = pd.DataFrame({
    'var1': [['a', 'b', 'c'], ['d', 'e',], [], np.nan], 
    'var2': [1, 2, 3, 4]
})
df
        var1  var2
0  [a, b, c]     1
1     [d, e]     2
2         []     3
3        NaN     4

df.explode('var1')

  var1  var2
0    a     1
0    b     1
0    c     1
1    d     2
1    e     2
2  NaN     3  # empty list converted to NaN
3  NaN     4  # NaN entry preserved as-is

# to reset the index to be monotonically increasing...
df.explode('var1').reset_index(drop=True)

  var1  var2
0    a     1
1    b     1
2    c     1
3    d     2
4    e     2
5  NaN     3
6  NaN     4

Notez que cela gère également les colonnes mixtes de listes et de scalaires, ainsi que les listes vides et les NaN de manière appropriée (c'est un inconvénient des repeatsolutions basées sur la base de données).

Cependant, vous devez noter que cela explodene fonctionne que sur une seule colonne (pour le moment).

PS: si vous cherchez à exploser une colonne de chaînes , vous devez d'abord diviser sur un séparateur, puis utiliser explode. Voir cette réponse (très) connexe de moi.

cs95
la source
8
Enfin, un explode () pour les Pandas!
Kai
2
enfin! L'esprit soufflé! Excellente réponse de @MaxU ci-dessus, mais cela simplifie beaucoup les choses.
accro
12

vous pouvez également utiliser pd.concatet pd.meltpour cela:

>>> objs = [df, pd.DataFrame(df['samples'].tolist())]
>>> pd.concat(objs, axis=1).drop('samples', axis=1)
   subject  trial_num     0     1     2
0        1          1 -0.49 -1.00  0.44
1        1          2 -0.28  1.48  2.01
2        1          3 -0.52 -1.84  0.02
3        2          1  1.23 -1.36 -1.06
4        2          2  0.54  0.18  0.51
5        2          3 -2.18 -0.13 -1.35
>>> pd.melt(_, var_name='sample_num', value_name='sample', 
...         value_vars=[0, 1, 2], id_vars=['subject', 'trial_num'])
    subject  trial_num sample_num  sample
0         1          1          0   -0.49
1         1          2          0   -0.28
2         1          3          0   -0.52
3         2          1          0    1.23
4         2          2          0    0.54
5         2          3          0   -2.18
6         1          1          1   -1.00
7         1          2          1    1.48
8         1          3          1   -1.84
9         2          1          1   -1.36
10        2          2          1    0.18
11        2          3          1   -0.13
12        1          1          2    0.44
13        1          2          2    2.01
14        1          3          2    0.02
15        2          1          2   -1.06
16        2          2          2    0.51
17        2          3          2   -1.35

Enfin, si vous avez besoin, vous pouvez trier en fonction des trois premières colonnes.

behzad.nouri
la source
1
Cela ne fonctionne que si vous savez a priori quelle sera la longueur des listes et / ou si elles auront toutes la même longueur?
Chill2Macht
9

En essayant de travailler pas à pas sur la solution de Roman Pekar pour mieux la comprendre, j'ai proposé ma propre solution, qui permet meltd'éviter une partie de l'empilement déroutant et de la réinitialisation d'index. Je ne peux pas dire que c'est évidemment une solution plus claire:

items_as_cols = df.apply(lambda x: pd.Series(x['samples']), axis=1)
# Keep original df index as a column so it's retained after melt
items_as_cols['orig_index'] = items_as_cols.index

melted_items = pd.melt(items_as_cols, id_vars='orig_index', 
                       var_name='sample_num', value_name='sample')
melted_items.set_index('orig_index', inplace=True)

df.merge(melted_items, left_index=True, right_index=True)

Sortie (évidemment, nous pouvons supprimer la colonne d'échantillons d'origine maintenant):

                 samples  subject  trial_num sample_num  sample
0    [1.84, 1.05, -0.66]        1          1          0    1.84
0    [1.84, 1.05, -0.66]        1          1          1    1.05
0    [1.84, 1.05, -0.66]        1          1          2   -0.66
1    [-0.24, -0.9, 0.65]        1          2          0   -0.24
1    [-0.24, -0.9, 0.65]        1          2          1   -0.90
1    [-0.24, -0.9, 0.65]        1          2          2    0.65
2    [1.15, -0.87, -1.1]        1          3          0    1.15
2    [1.15, -0.87, -1.1]        1          3          1   -0.87
2    [1.15, -0.87, -1.1]        1          3          2   -1.10
3   [-0.8, -0.62, -0.68]        2          1          0   -0.80
3   [-0.8, -0.62, -0.68]        2          1          1   -0.62
3   [-0.8, -0.62, -0.68]        2          1          2   -0.68
4    [0.91, -0.47, 1.43]        2          2          0    0.91
4    [0.91, -0.47, 1.43]        2          2          1   -0.47
4    [0.91, -0.47, 1.43]        2          2          2    1.43
5  [-1.14, -0.24, -0.91]        2          3          0   -1.14
5  [-1.14, -0.24, -0.91]        2          3          1   -0.24
5  [-1.14, -0.24, -0.91]        2          3          2   -0.91
Marius
la source
6

Pour ceux qui recherchent une version de la réponse de Roman Pekar qui évite la dénomination manuelle des colonnes:

column_to_explode = 'samples'
res = (df
       .set_index([x for x in df.columns if x != column_to_explode])[column_to_explode]
       .apply(pd.Series)
       .stack()
       .reset_index())
res = res.rename(columns={
          res.columns[-2]:'exploded_{}_index'.format(column_to_explode),
          res.columns[-1]: '{}_exploded'.format(column_to_explode)})
Charles Davis
la source
4

J'ai trouvé que le moyen le plus simple était de:

  1. Convertir la samplescolonne en DataFrame
  2. Rejoindre le df original
  3. Fusion

Montré ici:

    df.samples.apply(lambda x: pd.Series(x)).join(df).\
melt(['subject','trial_num'],[0,1,2],var_name='sample')

        subject  trial_num sample  value
    0         1          1      0  -0.24
    1         1          2      0   0.14
    2         1          3      0  -0.67
    3         2          1      0  -1.52
    4         2          2      0  -0.00
    5         2          3      0  -1.73
    6         1          1      1  -0.70
    7         1          2      1  -0.70
    8         1          3      1  -0.29
    9         2          1      1  -0.70
    10        2          2      1  -0.72
    11        2          3      1   1.30
    12        1          1      2  -0.55
    13        1          2      2   0.10
    14        1          3      2  -0.44
    15        2          1      2   0.13
    16        2          2      2  -1.44
    17        2          3      2   0.73

Il convient de noter que cela n'a peut-être fonctionné que parce que chaque essai a le même nombre d'échantillons (3). Quelque chose de plus intelligent peut être nécessaire pour les essais de différentes tailles d'échantillons.

Michael Silverstein
la source
2

Réponse très tardive mais je veux ajouter ceci:

Une solution rapide utilisant vanilla Python qui prend également en charge la sample_numcolonne dans l'exemple d'OP. Sur mon propre grand ensemble de données avec plus de 10 millions de lignes et un résultat avec 28 millions de lignes, cela ne prend que 38 secondes environ. La solution acceptée se décompose complètement avec cette quantité de données et conduit à un memory errorsur mon système qui a 128 Go de RAM.

df = df.reset_index(drop=True)
lstcol = df.lstcol.values
lstcollist = []
indexlist = []
countlist = []
for ii in range(len(lstcol)):
    lstcollist.extend(lstcol[ii])
    indexlist.extend([ii]*len(lstcol[ii]))
    countlist.extend([jj for jj in range(len(lstcol[ii]))])
df = pd.merge(df.drop("lstcol",axis=1),pd.DataFrame({"lstcol":lstcollist,"lstcol_num":countlist},
index=indexlist),left_index=True,right_index=True).reset_index(drop=True)
Khris
la source
2

Aussi très tard, mais voici une réponse de Karvy1 qui a bien fonctionné pour moi si vous n'avez pas de pandas> = version 0.25: https://stackoverflow.com/a/52511166/10740287

Pour l'exemple ci-dessus, vous pouvez écrire:

data = [(row.subject, row.trial_num, sample) for row in df.itertuples() for sample in row.samples]
data = pd.DataFrame(data, columns=['subject', 'trial_num', 'samples'])

Test de rapidité:

%timeit data = pd.DataFrame([(row.subject, row.trial_num, sample) for row in df.itertuples() for sample in row.samples], columns=['subject', 'trial_num', 'samples'])

1,33 ms ± 74,8 µs par boucle (moyenne ± écart type de 7 analyses, 1000 boucles chacune)

%timeit data = df.set_index(['subject', 'trial_num'])['samples'].apply(pd.Series).stack().reset_index()

4,9 ms ± 189 µs par boucle (moyenne ± écart type de 7 analyses, 100 boucles chacune)

%timeit data = pd.DataFrame({col:np.repeat(df[col].values, df['samples'].str.len())for col in df.columns.drop('samples')}).assign(**{'samples':np.concatenate(df['samples'].values)})

1,38 ms ± 25 µs par boucle (moyenne ± écart type de 7 analyses, 1000 boucles chacune)

Rémy Pétremand
la source
1
import pandas as pd
df = pd.DataFrame([{'Product': 'Coke', 'Prices': [100,123,101,105,99,94,98]},{'Product': 'Pepsi', 'Prices': [101,104,104,101,99,99,99]}])
print(df)
df = df.assign(Prices=df.Prices.str.split(',')).explode('Prices')
print(df)

Essayez ceci dans les pandas> = version 0.25

Tapas
la source
1
Pas besoin .str.split(',')car Pricesc'est déjà une liste.
Oren