Coluna de listas do Pandas, crie uma linha para cada elemento da lista

163

Eu tenho um quadro de dados em que algumas células contêm listas de vários valores. Em vez de armazenar vários valores em uma célula, eu gostaria de expandir o quadro de dados para que cada item da lista obtenha sua própria linha (com os mesmos valores em todas as outras colunas). Então, se eu tiver:

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

Como faço para converter para formato longo, por exemplo:

   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.

O índice não é importante, não há problema em definir colunas existentes como o índice e a ordem final não é importante.

Marius
fonte
11
No pandas 0.25, você também pode usar df.explode('samples')para resolver isso. explodesó pode suportar a explosão de uma coluna por enquanto.
precisa saber é o seguinte

Respostas:

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

PS aqui você pode encontrar uma solução um pouco mais genérica


UPDATE: algumas explicações: A IMO, a maneira mais fácil de entender esse código, é tentar executá-lo passo a passo:

na linha seguinte, estamos repetindo valores em uma coluna Nvezes onde N- é o comprimento da lista correspondente:

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)

isso pode ser generalizado para todas as colunas, contendo 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 achatar todos os valores na listcoluna ( samples) e obter um vetor 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])

colocando tudo isso 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]

usando pd.DataFrame()[df.columns]garantirá que estamos selecionando colunas na ordem original ...

MaxU
fonte
3
Essa deve ser a resposta aceita. A resposta atualmente aceita é muito, muito mais lenta em comparação com isso.
irene
1
Eu não consigo descobrir como consertar isso: TypeError: Não é possível lançar dados da matriz de dtipo ( 'float64') para dtipo ( 'int64') de acordo com a regra de 'seguro'
Greg
1
Esta é a única resposta que funcionou para mim, dentre as 10+ encontradas durante uma hora inteira pesquisando as pilhas. Obrigado MaxU 🙏
olisteadman 26/10/18
1
Observe que isso elimina lst_colinteiramente as linhas que possuem uma lista vazia ; para manter essas linhas e preencher sua lst_colcom np.nan, você pode apenas fazer df[lst_col] = df[lst_col].apply(lambda x: x if len(x) > 0 else [np.nan])antes de usar este método. Evidentemente .masknão retornará listas, daí o .apply.
Charles Davis
Esta é uma excelente resposta que deve ser a aceita. Embora seja uma resposta de nível de magia negra, e eu, por exemplo, apreciaria alguma explicação sobre o que essas etapas realmente fazem.
usar o seguinte código
129

Um pouco mais do que eu esperava:

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

Se você deseja um índice seqüencial, pode aplicar reset_index(drop=True)- se ao resultado.

update :

>>> 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
fonte
Obrigado, mesmo o primeiro passo da inscrição para colocar cada item em sua própria coluna é uma grande ajuda. Consegui criar uma maneira um pouco diferente de fazer isso, mas ainda há alguns passos envolvidos. Aparentemente, isso não é fácil de fazer nos pandas!
Marius
1
Ótima resposta. Você pode reduzi-lo um pouco substituindo df.apply(lambda x: pd.Series(x['samples']),axis=1)por df.samples.apply(pd.Series).
Dennis Golomazov
1
Nota para os leitores: isso sofre muito com problemas de desempenho. Veja aqui uma solução com muito mais desempenho usando o numpy.
cs95
2
qual é a solução quando o número de amostras não é o mesmo para todas as linhas?
22418 SarahData
@SarahData Use df.explode()como mostrado aqui.
cs95
63

Pandas> = 0,25

Os métodos Series e DataFrame definem um .explode()método que explode listas em linhas separadas. Consulte a seção de documentos em Explodindo uma coluna do 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

Observe que isso também lida com colunas mistas de listas e escalares, bem como listas vazias e NaNs adequadamente (essa é uma desvantagem de repeatsoluções baseadas em).

No entanto, observe que explodesó funciona em uma única coluna (por enquanto).

PS: se você estiver procurando explodir uma coluna de strings , primeiro precisará dividir em um separador e depois usar explode. Veja esta (muito) resposta relacionada por mim.

cs95
fonte
8
Finalmente, uma explosão () para os pandas!
Kai
2
finalmente! Mindblown! Ótima resposta do @MaxU acima, mas isso torna as coisas muito mais simplificadas.
viciado
12

você também pode usar pd.concate pd.meltpara isso:

>>> 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, se você precisar, pode classificar a base nas primeiras três primeiras colunas.

behzad.nouri
fonte
1
Isso só funciona se você souber a priori qual será o tamanho das listas e / ou se todas terão o mesmo tamanho?
Chill2Macht
9

Tentando trabalhar com a solução de Roman Pekar passo a passo para entendê-la melhor, criei minha própria solução, usada meltpara evitar parte do empilhamento confuso e da redefinição do índice. Não posso dizer que é obviamente uma solução mais 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)

Saída (obviamente, podemos soltar a coluna de amostras originais agora):

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

Para aqueles que procuram uma versão da resposta de Roman Pekar que evita a nomeação manual de colunas:

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

Eu achei a maneira mais fácil de:

  1. Converta a samplescoluna em um DataFrame
  2. Juntando-se ao df original
  3. Derretendo

Mostrado aqui:

    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 ressaltar que isso pode ter funcionado apenas porque cada tentativa tem o mesmo número de amostras (3). Algo mais inteligente pode ser necessário para ensaios com diferentes tamanhos de amostra.

Michael Silverstein
fonte
2

Resposta muito tardia, mas quero acrescentar isso:

Uma solução rápida usando Python baunilha que também cuida da sample_numcoluna no exemplo do OP. No meu próprio conjunto de dados grande, com mais de 10 milhões de linhas e um resultado com 28 milhões de linhas, isso leva apenas cerca de 38 segundos. A solução aceita se decompõe completamente com essa quantidade de dados e leva a um memory errorsistema que possui 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
fonte
2

Também muito tarde, mas aqui está uma resposta de Karvy1 que funcionou bem para mim se você não tiver pandas> = 0,25 versão: https://stackoverflow.com/a/52511166/10740287

Para o exemplo acima, você pode escrever:

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

Teste rápido:

%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 loop (média ± desvio padrão de 7 corridas, 1000 loops cada)

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

4,9 ms ± 189 µs por loop (média ± desvio padrão de 7 corridas, 100 loops cada)

%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 loop (média ± desvio padrão de 7 corridas, 1000 loops cada)

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

Tente isso em pandas> = 0,25 versão

Tapas
fonte
1
Não há necessidade, .str.split(',')porque Pricesjá é uma lista.
Oren