Cómo descomponer una lista dentro de una celda de marco de datos en filas separadas

93

Estoy buscando convertir una celda de pandas que contiene una lista en filas para cada uno de esos valores.

Entonces, toma esto:

ingrese la descripción de la imagen aquí

Si quisiera descomprimir y apilar los valores en la nearest_neighborscolumna para que cada valor sea una fila dentro de cada opponentíndice, ¿cuál es la mejor manera de hacerlo ? ¿Hay métodos pandas que estén destinados a operaciones como esta?

PicanteClubSalsa
fuente
¿Podría dar un ejemplo de la salida deseada y lo que ha intentado hasta ahora? Es más fácil para otros ayudarlo si proporciona algunos datos de muestra que también se pueden cortar y pegar.
Dagrha
Puede utilizar pd.DataFrame(df.nearest_neighbors.values.tolist())para descomprimir esta columna y luego pd.mergepegarla con las demás.
hellpanderr
@helpanderr no creo que values.tolist()haga nada aquí; la columna ya es una lista
maxymoo
2
@maxymoo i.imgur.com/YGQAYOY.png
hellpanderr
1
Relacionado pero contiene más detalles stackoverflow.com/questions/53218931/…
BEN_YO

Respuestas:

54

En el siguiente código, primero restablezco el índice para facilitar la iteración de la fila.

Creo una lista de listas donde cada elemento de la lista externa es una fila del DataFrame de destino y cada elemento de la lista interna es una de las columnas. Esta lista anidada finalmente se concatenará para crear el DataFrame deseado.

Utilizo una lambdafunción junto con la iteración de la lista para crear una fila para cada elemento del nearest_neighborsemparejado con el relevante namey opponent.

Finalmente, creo un nuevo DataFrame a partir de esta lista (usando los nombres de las columnas originales y volviendo a establecer el índice en namey 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 JUNIO 2017

Un método alternativo es el siguiente:

>>> (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()
     )
Alejandro
fuente
apply(pd.Series)está bien en los marcos más pequeños, pero para los marcos de tamaño razonable, debe reconsiderar una solución más eficaz. Consulte ¿ Cuándo debería usar pandas apply () en mi código? (Una mejor solución es enumerar la columna primero.)
cs95
2
La explosión de una columna similar a una lista se ha simplificado significativamente en pandas 0.25 con la adición del explode()método. Agregué una respuesta con un ejemplo usando la misma configuración de df que aquí.
joelostblom
@joelostblom Es bueno escuchar. Gracias por agregar el ejemplo con el uso actual.
Alexander
35
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')

Fuera:

                    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
joelostblom
fuente
2
Tenga en cuenta que esto solo funciona para una sola columna (a partir de 0.25). Consulte aquí y aquí para obtener soluciones más genéricas.
cs95
esta es la solución más fácil y rápida (de hecho, si solo tiene una columna con una lista para explotar o "desenrollar", como se llamaría en mongodb)
annakeuchenius
34

Utilice apply(pd.Series)y stack, luego reset_indexyto_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

Detalles

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]
Cero
fuente
1
¡Ama la elegancia de tu solución! ¿Lo ha comparado con otros enfoques por casualidad?
rpyzh
1
El resultado de df.nearest_neighbors.apply(pd.Series)es muy sorprendente para mí;
Calum You
1
@rpyzh Sí, es bastante elegante, pero patéticamente lento.
cs95
16

Creo que esta es una muy buena pregunta, en Hive que usaría EXPLODE, creo que se puede argumentar que Pandas debería incluir esta funcionalidad de forma predeterminada. Probablemente explotaría la columna de la lista con una comprensión de generador anidada 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"])
maxymoo
fuente
Me gusta cómo esta solución permite que la cantidad de elementos de la lista sea diferente para cada fila.
user1718097
¿Hay alguna forma de mantener el índice original con este método?
SummerEla
2
@SummerEla lol, esta fue una respuesta muy antigua, he actualizado para mostrar cómo lo haría ahora
maxymoo
1
@maxymoo Sin embargo, sigue siendo una gran pregunta. ¡Gracias por actualizar!
SummerEla
Encontré esto útil y lo convertí en un paquete
Oren
11

El método más rápido que encontré hasta ahora es extender el DataFrame .ilocy volver a asignar la columna de destino aplanada .

Dada la entrada habitual (replicada un poco):

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

Encuentro que extend_iloc()es el más 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)
Oleg
fuente
buena evaluación
javadba
2
Gracias por esto, realmente me ayudó. Usé la solución extend_iloc y encontré que cols = [c for c in df.columns if c != col_target] debería ser: cols = [i for i,c in enumerate(df.columns) if c != col_target] Los df.iloc[ilocations, cols].copy()errores si no se presentan con el índice de la columna.
jdungan
Gracias de nuevo por la sugerencia de iloc. Escribí una explicación detallada de cómo funciona aquí: medium.com/@johnadungan/… . Espero que ayude a cualquiera con un desafío similar.
jdungan
7

Mejor solución alternativa con aplicar (serie pd.):

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)
Philipp Schwarz
fuente
Éste expande columnas, no filas.
Oleg
@Oleg correcto, pero siempre puede transponer el DataFrame y luego aplicar pd.Series, mucho más simple que la mayoría de las otras sugerencias
Philipp Schwarz
7

Similar a la funcionalidad EXPLODE de 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
13 Herger
fuente
1
Cuando ejecuto esto, NameError: global name 'copy' is not defined
aparece
4

Entonces, todas estas respuestas son buenas, pero quería algo ^ realmente simple ^ así que aquí está mi contribución:

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

Eso es todo ... solo usa esto cuando quieras una nueva serie donde las listas se 'exploten'. Aquí hay un ejemplo donde hacemos value_counts () en las opciones de tacos :)

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
Briford Wylie
fuente
2

A continuación, se muestra una posible optimización para marcos de datos más grandes. Esto se ejecuta más rápido cuando hay varios valores iguales en el campo "explosivo". (Cuanto mayor sea el marco de datos en comparación con el recuento de valores únicos en el campo, mejor funcionará este 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
Sinan Ozel
fuente
1

Extendiendo la .ilocrespuesta de Oleg para aplanar automáticamente todas las columnas de 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

Esto supone que cada columna de lista tiene la misma longitud de lista.

Brian Atwood
fuente
1

En lugar de utilizar apply (pd.Series), puede aplanar la columna. Esto mejora el rendimiento.

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)
suleep kumar
fuente
IndexError: Demasiados niveles: el índice tiene solo 2 niveles, no 3, cuando pruebo mi ejemplo
vinsent paramanantham
1
Tienes que cambiar el "nivel" en reset_index de acuerdo con tu ejemplo
suleep kumar