Como explodir uma lista dentro de uma célula do Dataframe em linhas separadas


93

Estou tentando transformar uma célula do pandas contendo uma lista em linhas para cada um desses valores.

Então, pegue isto:

insira a descrição da imagem aqui

Se eu gostaria de descompactar e empilhar os valores na nearest_neighborscoluna de forma que cada valor fosse uma linha dentro de cada opponentíndice, qual seria a melhor maneira de fazer isso? Existem métodos do pandas destinados a operações como essa?


Você poderia dar um exemplo da saída desejada e o que tentou até agora? É mais fácil para outras pessoas ajudá-lo se você fornecer alguns dados de amostra que também podem ser recortados e colados.
dagrha,

Você pode usar pd.DataFrame(df.nearest_neighbors.values.tolist())para descompactar esta coluna e, em seguida, pd.mergecolá-la com as outras.
hellpanderr

@helpanderr acho values.tolist()que não faz nada aqui; a coluna já é uma lista
maxymoo


1
Relacionado, mas contém mais detalhes stackoverflow.com/questions/53218931/…
BEN_YO

Respostas:


54

No código abaixo, primeiro redefino o índice para tornar a iteração da linha mais fácil.

Eu crio uma lista de listas em que cada elemento da lista externa é uma linha do DataFrame de destino e cada elemento da lista interna é uma das colunas. Essa lista aninhada será finalmente concatenada para criar o DataFrame desejado.

Eu uso uma lambdafunção junto com a iteração de lista para criar uma linha para cada elemento do nearest_neighborspar com o namee relevante opponent.

Por fim, crio um novo DataFrame a partir dessa lista (usando os nomes das colunas originais e configurando o índice de volta para namee opponent).

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))

>>> df
                                                    nearest_neighbors
name       opponent                                                  
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]

df.reset_index(inplace=True)
rows = []
_ = df.apply(lambda row: [rows.append([row['name'], row['opponent'], nn]) 
                         for nn in row.nearest_neighbors], axis=1)
df_new = pd.DataFrame(rows, columns=df.columns).set_index(['name', 'opponent'])

>>> df_new
                    nearest_neighbors
name       opponent                  
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

EDITAR JUNHO DE 2017

Um método alternativo é o seguinte:

>>> (pd.melt(df.nearest_neighbors.apply(pd.Series).reset_index(), 
             id_vars=['name', 'opponent'],
             value_name='nearest_neighbors')
     .set_index(['name', 'opponent'])
     .drop('variable', axis=1)
     .dropna()
     .sort_index()
     )

apply(pd.Series)está bem no menor dos quadros, mas para qualquer quadro de tamanho razoável, você deve reconsiderar uma solução de melhor desempenho. Consulte Quando devo usar o pandas apply () em meu código? (Uma solução melhor é listar a coluna primeiro.)
cs95

2
Explodir uma coluna semelhante a uma lista foi simplificado significativamente no pandas 0,25 com a adição do explode()método. Eu adicionei uma resposta com um exemplo usando a mesma configuração de df como aqui.
joelostblom

@joelostblom Bom saber. Obrigado por adicionar o exemplo com o uso atual.
Alexander

34

Use apply(pd.Series)e stack, então reset_indexeto_frame

In [1803]: (df.nearest_neighbors.apply(pd.Series)
              .stack()
              .reset_index(level=2, drop=True)
              .to_frame('nearest_neighbors'))
Out[1803]:
                    nearest_neighbors
name       opponent
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

Detalhes

In [1804]: df
Out[1804]:
                                                   nearest_neighbors
name       opponent
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]

1
Ame a elegância da sua solução! Você já comparou isso com outras abordagens por acaso?
rpyzh

1
O resultado de df.nearest_neighbors.apply(pd.Series)é muito surpreendente para mim;
Calum You

1
@rpyzh Sim, é bastante elegante, mas pateticamente lento.
cs95

33
df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))

df.explode('nearest_neighbors')

Fora:

                    nearest_neighbors
name       opponent                  
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

2
Observe que isso só funciona para uma única coluna (a partir de 0,25). Veja aqui e aqui para mais soluções genéricas.
cs95 de

16

Eu acho que essa é uma pergunta muito boa, no Hive você usaria EXPLODE, eu acho que é preciso argumentar que o Pandas deve incluir essa funcionalidade por padrão. Eu provavelmente explodiria a coluna da lista com uma compreensão de gerador aninhado como esta:

pd.DataFrame({
    "name": i[0],
    "opponent": i[1],
    "nearest_neighbor": neighbour
    }
    for i, row in df.iterrows() for neighbour in row.nearest_neighbors
    ).set_index(["name", "opponent"])

Gosto de como essa solução permite que o número de itens da lista seja diferente para cada linha.
user1718097

Existe uma maneira de manter o índice original com este método?
SummerEla de

2
@SummerEla lol esta era uma resposta muito antiga, eu atualizei para mostrar como eu faria isso agora
maxymoo

1
@maxymoo Ainda é uma ótima pergunta, no entanto. Obrigado por atualizar!
SummerEla

Achei isso útil e transformei-o em um pacote
Oren

11

O método mais rápido que encontrei até agora é estender o DataFrame com .iloce atribuir de volta o plano coluna alvo.

Dada a entrada usual (replicada um pouco):

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))
df = pd.concat([df]*10)

df
Out[3]: 
                                                   nearest_neighbors
name       opponent                                                 
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
...

Dadas as seguintes alternativas sugeridas:

col_target = 'nearest_neighbors'

def extend_iloc():
    # Flatten columns of lists
    col_flat = [item for sublist in df[col_target] for item in sublist] 
    # Row numbers to repeat 
    lens = df[col_target].apply(len)
    vals = range(df.shape[0])
    ilocations = np.repeat(vals, lens)
    # Replicate rows and add flattened column of lists
    cols = [i for i,c in enumerate(df.columns) if c != col_target]
    new_df = df.iloc[ilocations, cols].copy()
    new_df[col_target] = col_flat
    return new_df

def melt():
    return (pd.melt(df[col_target].apply(pd.Series).reset_index(), 
             id_vars=['name', 'opponent'],
             value_name=col_target)
            .set_index(['name', 'opponent'])
            .drop('variable', axis=1)
            .dropna()
            .sort_index())

def stack_unstack():
    return (df[col_target].apply(pd.Series)
            .stack()
            .reset_index(level=2, drop=True)
            .to_frame(col_target))

Acho que extend_iloc()é o mais rápido :

%timeit extend_iloc()
3.11 ms ± 544 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit melt()
22.5 ms ± 1.25 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit stack_unstack()
11.5 ms ± 410 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

boa avaliação
javadba

2
Obrigado por isso, realmente me ajudou. Usei a solução extend_iloc e descobri que cols = [c for c in df.columns if c != col_target] deveria ser: cols = [i for i,c in enumerate(df.columns) if c != col_target] Os df.iloc[ilocations, cols].copy()erros não são apresentados com o índice da coluna.
jdungan,

Obrigado novamente pela sugestão do iloc. Eu escrevi uma explicação detalhada de como funciona aqui: medium.com/@johnadungan/… . Espero que ajude alguém com um desafio semelhante.
jdungan

7

Solução alternativa mais agradável com aplicar (pd.Series):

df = pd.DataFrame({'listcol':[[1,2,3],[4,5,6]]})

# expand df.listcol into its own dataframe
tags = df['listcol'].apply(pd.Series)

# rename each variable is listcol
tags = tags.rename(columns = lambda x : 'listcol_' + str(x))

# join the tags dataframe back to the original dataframe
df = pd.concat([df[:], tags[:]], axis=1)

Este expande colunas, não linhas.
Oleg de

@Oleg certo, mas você sempre pode transpor o DataFrame e, em seguida, aplicar pd.Series - de forma mais simples do que a maioria das outras sugestões
Philipp Schwarz

7

Semelhante à funcionalidade EXPLODE do Hive:

import copy

def pandas_explode(df, column_to_explode):
    """
    Similar to Hive's EXPLODE function, take a column with iterable elements, and flatten the iterable to one element 
    per observation in the output table

    :param df: A dataframe to explod
    :type df: pandas.DataFrame
    :param column_to_explode: 
    :type column_to_explode: str
    :return: An exploded data frame
    :rtype: pandas.DataFrame
    """

    # Create a list of new observations
    new_observations = list()

    # Iterate through existing observations
    for row in df.to_dict(orient='records'):

        # Take out the exploding iterable
        explode_values = row[column_to_explode]
        del row[column_to_explode]

        # Create a new observation for every entry in the exploding iterable & add all of the other columns
        for explode_value in explode_values:

            # Deep copy existing observation
            new_observation = copy.deepcopy(row)

            # Add one (newly flattened) value from exploding iterable
            new_observation[column_to_explode] = explode_value

            # Add to the list of new observations
            new_observations.append(new_observation)

    # Create a DataFrame
    return_df = pandas.DataFrame(new_observations)

    # Return
    return return_df

1
Quando executo isso, recebo o seguinte erro:NameError: global name 'copy' is not defined
frmsaul

4

Todas essas respostas são boas, mas eu queria algo ^ realmente simples ^ então aqui está minha contribuição:

def explode(series):
    return pd.Series([x for _list in series for x in _list])                               

É isso .. apenas use isso quando quiser uma nova série onde as listas são 'explodidas'. Aqui está um exemplo onde fazemos value_counts () nas escolhas do taco :)

In [1]: my_df = pd.DataFrame(pd.Series([['a','b','c'],['b','c'],['c']]), columns=['tacos'])      
In [2]: my_df.head()                                                                               
Out[2]: 
   tacos
0  [a, b, c]
1     [b, c]
2        [c]

In [3]: explode(my_df['tacos']).value_counts()                                                     
Out[3]: 
c    3
b    2
a    1

2

Aqui está uma otimização potencial para dataframes maiores. Isso é executado mais rápido quando há vários valores iguais no campo "explodindo". (Quanto maior for o dataframe em comparação com a contagem de valor exclusivo no campo, melhor será o desempenho desse código.)

def lateral_explode(dataframe, fieldname): 
    temp_fieldname = fieldname + '_made_tuple_' 
    dataframe[temp_fieldname] = dataframe[fieldname].apply(tuple)       
    list_of_dataframes = []
    for values in dataframe[temp_fieldname].unique().tolist(): 
        list_of_dataframes.append(pd.DataFrame({
            temp_fieldname: [values] * len(values), 
            fieldname: list(values), 
        }))
    dataframe = dataframe[list(set(dataframe.columns) - set([fieldname]))]\ 
        .merge(pd.concat(list_of_dataframes), how='left', on=temp_fieldname) 
    del dataframe[temp_fieldname]

    return dataframe

1

Estendendo a .ilocresposta de Oleg para nivelar automaticamente todas as colunas da lista:

def extend_iloc(df):
    cols_to_flatten = [colname for colname in df.columns if 
    isinstance(df.iloc[0][colname], list)]
    # Row numbers to repeat 
    lens = df[cols_to_flatten[0]].apply(len)
    vals = range(df.shape[0])
    ilocations = np.repeat(vals, lens)
    # Replicate rows and add flattened column of lists
    with_idxs = [(i, c) for (i, c) in enumerate(df.columns) if c not in cols_to_flatten]
    col_idxs = list(zip(*with_idxs)[0])
    new_df = df.iloc[ilocations, col_idxs].copy()

    # Flatten columns of lists
    for col_target in cols_to_flatten:
        col_flat = [item for sublist in df[col_target] for item in sublist]
        new_df[col_target] = col_flat

    return new_df

Isso pressupõe que cada coluna da lista tenha o mesmo comprimento de lista.


1

Em vez de usar apply (pd.Series), você pode nivelar a coluna. Isso melhora o desempenho.

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                'opponent': ['76ers', 'blazers', 'bobcats'], 
                'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
  .set_index(['name', 'opponent']))



%timeit (pd.DataFrame(df['nearest_neighbors'].values.tolist(), index = df.index)
           .stack()
           .reset_index(level = 2, drop=True).to_frame('nearest_neighbors'))

1.87 ms ± 9.74 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)


%timeit (df.nearest_neighbors.apply(pd.Series)
          .stack()
          .reset_index(level=2, drop=True)
          .to_frame('nearest_neighbors'))

2.73 ms ± 16.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

IndexError: Muitos níveis: o índice tem apenas 2 níveis, não 3, quando tento meu exemplo
vinsent paramanantham

1
Você deve alterar o "nível" em reset_index de acordo com o seu exemplo
suleep kumar
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.