Come esplodere un elenco all'interno di una cella Dataframe in righe separate


93

Sto cercando di trasformare una cella panda contenente un elenco in righe per ciascuno di quei valori.

Quindi, prendi questo:

inserisci qui la descrizione dell'immagine

Se desidero decomprimere e impilare i valori nella nearest_neighborscolonna in modo che ogni valore sia una riga all'interno di ogni opponentindice, come dovrei farlo al meglio? Esistono metodi panda pensati per operazioni come questa?


Potresti fornire un esempio del risultato desiderato e di ciò che hai provato finora? È più facile per gli altri aiutarti se fornisci alcuni dati di esempio che possono essere tagliati e incollati.
dagrha

Puoi usare pd.DataFrame(df.nearest_neighbors.values.tolist())per decomprimere questa colonna e poi pd.mergeincollarla con le altre.
hellpanderr

@helpanderr penso che values.tolist()non faccia niente qui; la colonna è già una lista
maxymoo


1
Related ma contengono maggiori dettagli stackoverflow.com/questions/53218931/...
BEN_YO

Risposte:


54

Nel codice seguente, ho prima reimpostato l'indice per rendere più semplice l'iterazione della riga.

Creo un elenco di elenchi in cui ogni elemento dell'elenco esterno è una riga del DataFrame di destinazione e ogni elemento dell'elenco interno è una delle colonne. Questo elenco nidificato verrà infine concatenato per creare il DataFrame desiderato.

Uso una lambdafunzione insieme all'iterazione della lista per creare una riga per ogni elemento della nearest_neighborscoppia con il pertinente namee opponent.

Infine, creo un nuovo DataFrame da questo elenco (utilizzando i nomi delle colonne originali e reimpostando l'indice su 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

EDIT GIUGNO 2017

Un metodo alternativo è il seguente:

>>> (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)va bene sul più piccolo dei frame, ma per qualsiasi frame di dimensioni ragionevoli, dovresti riconsiderare una soluzione più performante. Vedi Quando dovrei usare panda apply () nel mio codice? (Una soluzione migliore è elencare prima la colonna.)
cs95

2
L'esplosione di una colonna simile a un elenco è stata semplificata in modo significativo in panda 0.25 con l'aggiunta del explode()metodo. Ho aggiunto una risposta con un esempio utilizzando la stessa configurazione df come qui.
joelostblom

@joelostblom Buono a sapersi. Grazie per aver aggiunto l'esempio con l'utilizzo corrente.
Alexander

34

Usa apply(pd.Series)e stack, quindi 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

Dettagli

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
Ama l'eleganza della tua soluzione! L'hai confrontato per caso con altri approcci?
rpyzh

1
Il risultato di df.nearest_neighbors.apply(pd.Series)è molto sorprendente per me;
Calum You

1
@rpyzh Sì, è abbastanza elegante, ma 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')

Su:

                    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
Nota che questo funziona solo per una singola colonna (a partire da 0,25). Vedi qui e qui per soluzioni più generiche.
cs95

16

Penso che questa sia davvero una buona domanda, in Hive useresti EXPLODE, penso che sia opportuno sostenere che Panda dovrebbe includere questa funzionalità per impostazione predefinita. Probabilmente farei esplodere la colonna dell'elenco con una comprensione del generatore annidata come questa:

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

Mi piace il modo in cui questa soluzione consente al numero di elementi dell'elenco di essere diverso per ogni riga.
user1718097

C'è un modo per mantenere l'indice originale con questo metodo?
SummerEla

2
@SummerEla lol questa era una risposta molto vecchia, ho aggiornato per mostrare come lo farei ora
maxymoo

1
@maxymoo È comunque una bella domanda. Grazie per l'aggiornamento!
SummerEla

L'ho trovato utile e l'ho trasformato in un pacchetto
Oren

11

Il metodo più veloce che ho trovato finora è l'estensione del DataFrame .iloce l'assegnazione della colonna di destinazione appiattita .

Dato il solito input (replicato un po '):

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

Date le seguenti alternative suggerite:

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

Trovo che extend_iloc()sia il più veloce :

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

bella valutazione
javadba

2
Grazie per questo, mi ha davvero aiutato. Ho usato la soluzione extent_iloc e ho scoperto che cols = [c for c in df.columns if c != col_target] dovrebbe essere: cols = [i for i,c in enumerate(df.columns) if c != col_target] Gli df.iloc[ilocations, cols].copy()errori se non presentati con l'indice della colonna.
jdungan

Grazie ancora per il suggerimento iloc. Ho scritto una spiegazione dettagliata di come funziona qui: medium.com/@johnadungan/… . Spero che aiuti chiunque abbia una sfida simile.
jdungan

7

Soluzione alternativa più piacevole con applica (pd. Serie):

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)

Questo espande le colonne non le righe.
Oleg

@Oleg giusto, ma puoi sempre trasporre il DataFrame e quindi applicare pd.Series -molto più semplice della maggior parte degli altri suggerimenti
Philipp Schwarz

7

Simile alla funzionalità EXPLODE di 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 lo NameError: global name 'copy' is not defined
eseguo

4

Quindi tutte queste risposte sono buone ma volevo qualcosa di molto semplice, quindi ecco il mio contributo:

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

Ecco fatto .. usalo quando vuoi una nuova serie in cui gli elenchi sono "esplosi". Ecco un esempio in cui facciamo value_counts () sulle scelte di 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

Ecco una potenziale ottimizzazione per dataframe più grandi. Funziona più velocemente quando ci sono diversi valori uguali nel campo "esplodente". (Più grande è il dataframe rispetto al conteggio del valore univoco nel campo, migliori saranno le prestazioni di questo codice.)

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

Estensione di Oleg .iloc risposta per appiattire automaticamente tutte le colonne della 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

Ciò presuppone che ogni colonna della lista abbia la stessa lunghezza della lista.


1

Invece di usare apply (pd.Series) puoi appiattire la colonna. Ciò migliora le prestazioni.

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: Troppi livelli: Index ha solo 2 livelli, non 3, quando provo il mio esempio
vinsent paramanantham

1
Devi cambiare "livello" in reset_index secondo il tuo esempio
suleep kumar il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.