Comment décomposer une liste à l'intérieur d'une cellule Dataframe en lignes séparées

93

Je cherche à transformer une cellule pandas contenant une liste en lignes pour chacune de ces valeurs.

Alors, prenez ceci:

entrez la description de l'image ici

Si je souhaite décompresser et empiler les valeurs dans la nearest_neighborscolonne de sorte que chaque valeur soit une ligne dans chaque opponentindex, comment procéder au mieux? Existe-t-il des méthodes pandas destinées à des opérations comme celle-ci?

SpicyClubSauce
la source
Pouvez-vous donner un exemple de la sortie souhaitée et de ce que vous avez essayé jusqu'à présent? Il est plus facile pour les autres de vous aider si vous fournissez des exemples de données qui peuvent également être copiés et collés.
dagrha
Vous pouvez utiliser pd.DataFrame(df.nearest_neighbors.values.tolist())pour décompresser cette colonne puis la pd.mergecoller avec les autres.
hellpanderr
@helpanderr je ne pense pas faire values.tolist()quoi que ce soit ici; la colonne est déjà une liste
maxymoo
2
@maxymoo i.imgur.com/YGQAYOY.png
hellpanderr
1
Liés mais contiennent plus de détails stackoverflow.com/questions/53218931/…
BEN_YO

Réponses:

54

Dans le code ci-dessous, je réinitialise d'abord l'index pour faciliter l'itération des lignes.

Je crée une liste de listes où chaque élément de la liste externe est une ligne du DataFrame cible et chaque élément de la liste interne est l'une des colonnes. Cette liste imbriquée sera finalement concaténée pour créer le DataFrame souhaité.

J'utilise une lambdafonction avec une itération de liste pour créer une ligne pour chaque élément de l' nearest_neighborsapparié avec le nameet opponent.

Enfin, je crée un nouveau DataFrame à partir de cette liste (en utilisant les noms de colonne d'origine et en définissant l'index sur nameet 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 JUIN 2017

Une autre méthode est la suivante:

>>> (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()
     )
Alexandre
la source
apply(pd.Series)est bien sur le plus petit des cadres, mais pour tous les cadres de taille raisonnable, vous devriez reconsidérer une solution plus performante. Voir Quand devrais-je utiliser pandas apply () dans mon code? (Une meilleure solution consiste à lister la colonne en premier.)
cs95
2
L'éclatement d'une colonne de type liste a été considérablement simplifié dans pandas 0.25 avec l'ajout de la explode()méthode. J'ai ajouté une réponse avec un exemple utilisant la même configuration df qu'ici.
joelostblom
@joelostblom C'est bon à entendre. Merci d'avoir ajouté l'exemple avec l'utilisation actuelle.
Alexander
34

Utilisez apply(pd.Series)et stack, puis reset_indexetto_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

Détails

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]
Zéro
la source
1
Aimez l'élégance de votre solution! L'avez-vous comparé à d'autres approches par hasard?
rpyzh
1
Le résultat df.nearest_neighbors.apply(pd.Series)est très étonnant pour moi;
Calum You
1
@rpyzh Oui, c'est assez élégant, mais pathétiquement lent.
cs95
34
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')

En dehors:

                    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
la source
2
Notez que cela ne fonctionne que pour une seule colonne (à partir de 0,25). Voir ici et ici pour des solutions plus génériques.
cs95
c'est la solution la plus simple et la plus rapide (en effet si vous n'avez qu'une seule colonne avec une liste à exploser ou "à dérouler" comme on l'appellerait dans mongodb)
annakeuchenius
16

Je pense que c'est une très bonne question, dans Hive, vous utiliseriez EXPLODE, je pense qu'il y a un cas à faire que Pandas devrait inclure cette fonctionnalité par défaut. J'éclaterais probablement la colonne de liste avec une compréhension de générateur imbriquée comme celle-ci:

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
la source
J'aime la façon dont cette solution permet au nombre d'éléments de liste d'être différent pour chaque ligne.
user1718097
Existe-t-il un moyen de conserver l'index d'origine avec cette méthode?
SummerEla
2
@SummerEla lol c'était une très vieille réponse, j'ai mis à jour pour montrer comment je le ferais maintenant
maxymoo
1
@maxymoo C'est toujours une excellente question, cependant. Merci pour la mise à jour!
SummerEla
J'ai trouvé cela utile et je l'ai transformé en package
Oren
11

La méthode la plus rapide que j'ai trouvée jusqu'à présent consiste à étendre le DataFrame avec .ilocet à attribuer à nouveau la colonne cible aplatie .

Compte tenu de l'entrée habituelle (répliquée un peu):

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

Compte tenu des alternatives suggérées suivantes:

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

Je trouve que extend_iloc()c'est le plus rapide :

%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
la source
belle évaluation
javadba
2
Merci pour cela, cela m'a vraiment aidé. J'ai utilisé la solution extend_iloc et j'ai trouvé que cela cols = [c for c in df.columns if c != col_target] devrait être: cols = [i for i,c in enumerate(df.columns) if c != col_target] Les df.iloc[ilocations, cols].copy()erreurs si elles ne sont pas présentées avec l'index de la colonne.
jdungan
Merci encore pour la suggestion iloc. J'ai écrit une explication détaillée de son fonctionnement ici: medium.com/@johnadungan/… . J'espère que cela aidera n'importe qui avec un défi similaire.
jdungan
7

Solution alternative plus agréable avec apply (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)
Philipp Schwarz
la source
Celui-ci développe les colonnes et non les lignes.
Oleg
@Oleg à droite, mais vous pouvez toujours transposer le DataFrame et ensuite appliquer pd.Series -way plus simple que la plupart des autres suggestions
Philipp Schwarz
7

Similaire à la fonctionnalité 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
13Herger
la source
1
Quand je lance ceci, j'obtiens l'erreur suivante:NameError: global name 'copy' is not defined
frmsaul
4

Donc toutes ces réponses sont bonnes mais je voulais quelque chose ^ vraiment simple ^ alors voici ma contribution:

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

C'est tout ... utilisez simplement ceci quand vous voulez une nouvelle série où les listes sont «éclatées». Voici un exemple où nous faisons value_counts () sur les choix 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
la source
2

Voici une optimisation potentielle pour des dataframes plus volumineux. Cela s'exécute plus rapidement lorsqu'il y a plusieurs valeurs égales dans le champ "éclaté". (Plus la trame de données est grande par rapport au nombre de valeurs uniques dans le champ, meilleures seront les performances de ce code.)

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
la source
1

Extension de la .ilocréponse d' Oleg pour aplatir automatiquement toutes les colonnes de liste:

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

Cela suppose que chaque colonne de liste a la même longueur de liste.

Brian Atwood
la source
1

Au lieu d'utiliser apply (pd.Series), vous pouvez aplatir la colonne. Cela améliore les performances.

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
la source
IndexError: Too many levels: Index n'a que 2 niveaux, pas 3, quand j'essaye mon exemple
vinsent paramanantham
1
Vous devez changer le "niveau" dans reset_index selon votre exemple
suleep kumar