Columna de listas de pandas, cree una fila para cada elemento de la lista

163

Tengo un marco de datos donde algunas celdas contienen listas de múltiples valores. En lugar de almacenar múltiples valores en una celda, me gustaría expandir el marco de datos para que cada elemento de la lista obtenga su propia fila (con los mismos valores en todas las demás columnas). Entonces si tengo:

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

¿Cómo convierto a forma larga, por ejemplo:

   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.

El índice no es importante, está bien establecer columnas existentes como índice y el orden final no es importante.

Mario
fuente
11
De pandas 0.25 también puedes usar df.explode('samples')para resolver esto. explodesolo puede soportar explotar una columna por ahora.
cs95

Respuestas:

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]

Resultado:

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

PD: aquí puede encontrar una solución un poco más genérica


ACTUALIZACIÓN: algunas explicaciones: en mi opinión, la forma más fácil de entender este código es intentar ejecutarlo paso a paso:

en la siguiente línea estamos repitiendo valores en una columna por Ndonde N- es la longitud de la lista correspondiente:

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)

Esto se puede generalizar para todas las columnas, que contienen valores escalares:

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]

usando np.concatenate()podemos aplanar todos los valores en la listcolumna ( samples) y obtener un vector 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])

poniendo todo esto junto:

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]

el uso pd.DataFrame()[df.columns]garantizará que estemos seleccionando columnas en el orden original ...

MaxU
fuente
3
Esta debería ser la respuesta aceptada. La respuesta actualmente aceptada es mucho, mucho más lenta en comparación con esto.
irene
1
No puedo encontrar la manera de solucionar esto: TypeError: No se pueden transmitir datos de matriz de dtype ('float64') a dtype ('int64') de acuerdo con la regla 'seguro'
Greg
1
Esta es la única respuesta que funcionó para mí, de los más de 10 encontrados durante una hora completa de búsqueda en las pilas. Gracias MaxU 🙏
olisteadman
1
Tenga en cuenta que esto elimina las filas que tienen una lista vacía por lst_colcompleto; para mantener estas filas y poblar su lst_colcon np.nan, sólo puede hacer df[lst_col] = df[lst_col].apply(lambda x: x if len(x) > 0 else [np.nan])antes de utilizar este método. Evidentemente .maskno devolverá listas, de ahí el .apply.
Charles Davis el
Esta es una excelente respuesta que debería ser la aceptada. Aunque, es una respuesta de nivel de magia negra, y yo, por mi parte, agradecería alguna explicación de lo que de hecho hacen estos pasos.
ifly6
129

Un poco más de lo que esperaba:

>>> 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 desea un índice secuencial, puede aplicarlo reset_index(drop=True)al resultado.

actualización :

>>> 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
fuente
Gracias, incluso el primer paso de aplicar para obtener cada elemento en su propia columna es de gran ayuda. Pude encontrar una forma ligeramente diferente de hacerlo, pero todavía hay algunos pasos justos involucrados. ¡Aparentemente esto no es sencillo de hacer en Pandas!
Marius
1
Gran respuesta. Puedes acortarlo un poco reemplazándolo df.apply(lambda x: pd.Series(x['samples']),axis=1)con df.samples.apply(pd.Series).
Dennis Golomazov
1
Nota para los lectores: esto sufre terriblemente por problemas de rendimiento. Consulte aquí para obtener una solución mucho más eficaz con numpy.
cs95
2
¿Cuál es la solución cuando el número de muestras no es el mismo para todas las filas?
SarahData
@SarahData Use df.explode()como se muestra aquí.
cs95
63

Pandas> = 0.25

Los métodos Series y DataFrame definen un .explode()método que explota las listas en filas separadas. Consulte la sección de documentos sobre Desglosar una columna tipo lista .

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

Tenga en cuenta que esto también maneja columnas mixtas de listas y escalares, así como listas vacías y NaN de manera adecuada (esto es un inconveniente de las repeatsoluciones basadas).

Sin embargo, debe tener en cuenta que explodesolo funciona en una sola columna (por ahora).

PD: si está buscando explotar una columna de cadenas , primero debe dividir en un separador y luego usar explode. Vea esta (muy) respuesta relacionada de mí.

cs95
fuente
8
¡Finalmente, una explosión () para Pandas!
Kai
2
¡finalmente! Mindblown! Gran respuesta de @MaxU arriba, pero esto simplifica mucho las cosas.
adicto
12

También puedes usar pd.concaty pd.meltpara esto:

>>> 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

Por último, si lo necesita, puede ordenar la base de las primeras tres primeras columnas.

behzad.nouri
fuente
1
Esto solo funciona si sabes a priori cuál será la longitud de las listas y / o si todas tendrán la misma longitud.
Chill2Macht
9

Tratando de trabajar paso a paso a través de la solución de Roman Pekar para comprenderla mejor, se me ocurrió mi propia solución, que se usa meltpara evitar algunos de los confusos apilamientos y restablecimientos de índices. Sin embargo, no puedo decir que obviamente sea una solución más clara:

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)

Salida (obviamente, podemos soltar la columna de muestras originales ahora):

                 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
Mario
fuente
6

Para aquellos que buscan una versión de la respuesta de Roman Pekar que evite el nombramiento manual de columnas:

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
fuente
4

Encontré que la forma más fácil era:

  1. Convierta la samplescolumna en un DataFrame
  2. Unirse con el df original
  3. Derritiendo

Mostrado aquí:

    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

Vale la pena señalar que esto puede haber funcionado solo porque cada ensayo tiene el mismo número de muestras (3). Puede ser necesario algo más inteligente para los ensayos de diferentes tamaños de muestra.

Michael Silverstein
fuente
2

Respuesta muy tardía pero quiero agregar esto:

Una solución rápida con Vanilla Python que también se encarga de la sample_numcolumna en el ejemplo de OP. En mi propio conjunto de datos grande con más de 10 millones de filas y un resultado con 28 millones de filas, esto solo lleva unos 38 segundos. La solución aceptada se descompone por completo con esa cantidad de datos y conduce a un memory erroren mi sistema que tiene 128 GB 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
fuente
2

También es muy tarde, pero aquí hay una respuesta de Karvy1 que funcionó bien para mí si no tienes pandas> = 0.25 versión: https://stackoverflow.com/a/52511166/10740287

Para el ejemplo anterior, puede escribir:

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'])

Prueba de velocidad:

%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 por bucle (media ± desviación estándar de 7 corridas, 1000 bucles cada una)

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

4.9 ms ± 189 µs por bucle (media ± desviación estándar de 7 corridas, 100 bucles cada una)

%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 por bucle (media ± desviación estándar de 7 corridas, 1000 bucles cada una)

Rémy Pétremand
fuente
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)

Prueba esto en pandas> = versión 0.25

Tapas
fuente
1
No es necesario .str.split(',')porque Pricesya es una lista.
Oren