So zerlegen Sie eine Liste in einer Dataframe-Zelle in separate Zeilen

93

Ich möchte eine Pandas-Zelle mit einer Liste in Zeilen für jeden dieser Werte umwandeln.

Also, nimm das:

Geben Sie hier die Bildbeschreibung ein

Wenn ich die Werte in der nearest_neighborsSpalte entpacken und stapeln möchte, sodass jeder Wert eine Zeile in jedem opponentIndex ist, wie würde ich am besten vorgehen? Gibt es Pandas-Methoden, die für solche Operationen gedacht sind?

SpicyClubSauce
quelle
Können Sie ein Beispiel für Ihre gewünschte Ausgabe geben und was Sie bisher versucht haben? Für andere ist es am einfachsten, Ihnen zu helfen, wenn Sie einige Beispieldaten bereitstellen, die auch ausgeschnitten und eingefügt werden können.
Dagrha
Sie können pd.DataFrame(df.nearest_neighbors.values.tolist())diese Spalte entpacken und dann pd.mergemit den anderen zusammenkleben.
Hellpanderr
@helpanderr Ich glaube values.tolist(), hier macht nichts; Die Spalte ist bereits eine Liste
Maxymoo
2
@maxymoo i.imgur.com/YGQAYOY.png
hellpanderr
1
Verwandte, aber mehr Details enthalten stackoverflow.com/questions/53218931/…
BEN_YO

Antworten:

54

Im folgenden Code habe ich zuerst den Index zurückgesetzt, um die Zeileniteration zu vereinfachen.

Ich erstelle eine Liste von Listen, in der jedes Element der äußeren Liste eine Zeile des Zieldatenrahmens und jedes Element der inneren Liste eine der Spalten ist. Diese verschachtelte Liste wird letztendlich verkettet, um den gewünschten DataFrame zu erstellen.

Ich benutze eine lambdaFunktion zusammen mit einer Listeniteration, um eine Zeile für jedes Element des nearest_neighborsgepaarten mit dem relevanten nameund zu erstellen opponent.

Schließlich erstelle ich aus dieser Liste einen neuen DataFrame (unter Verwendung der ursprünglichen Spaltennamen und Zurücksetzen des Index auf nameund 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 JUNI 2017

Eine alternative Methode ist wie folgt:

>>> (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()
     )
Alexander
quelle
apply(pd.Series)ist auf kleinsten Frames in Ordnung, aber für Frames mit angemessener Größe sollten Sie eine leistungsfähigere Lösung überdenken. Siehe Wann sollte ich jemals pandas apply () in meinem Code verwenden? (Eine bessere Lösung besteht darin, zuerst die Spalte aufzulisten.)
cs95
2
Das Auflösen einer listenartigen Spalte wurde in Pandas 0.25 durch Hinzufügen der explode()Methode erheblich vereinfacht . Ich habe eine Antwort mit einem Beispiel hinzugefügt , das dasselbe df-Setup wie hier verwendet.
Joelostblom
@joelostblom Gut zu hören. Vielen Dank, dass Sie das Beispiel mit der aktuellen Verwendung hinzugefügt haben.
Alexander
34

Verwenden Sie apply(pd.Series)und stack, dann reset_indexundto_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

Einzelheiten

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]
Null
quelle
1
Lieben Sie die Eleganz Ihrer Lösung! Haben Sie es zufällig mit anderen Ansätzen verglichen?
Rpyzh
1
Das Ergebnis von df.nearest_neighbors.apply(pd.Series)ist für mich sehr erstaunlich;
Calum Sie
1
@rpyzh Ja, es ist ziemlich elegant, aber erbärmlich langsam.
CS95
32
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')

Aus:

                    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
quelle
2
Beachten Sie, dass dies nur für eine einzelne Spalte funktioniert (ab 0,25). Siehe hier und hier für weitere generische Lösungen.
CS95
16

Ich denke, dies ist eine wirklich gute Frage. In Hive würden Sie sie verwenden EXPLODE. Ich denke, es ist zu begründen, dass Pandas diese Funktionalität standardmäßig enthalten sollten. Ich würde wahrscheinlich die Listenspalte mit einem verschachtelten Generatorverständnis wie folgt auflösen:

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
quelle
Mir gefällt, wie diese Lösung ermöglicht, dass die Anzahl der Listenelemente für jede Zeile unterschiedlich ist.
user1718097
Gibt es eine Möglichkeit, den ursprünglichen Index mit dieser Methode beizubehalten?
SummerEla
2
@ SummerEla lol das war eine wirklich alte Antwort, ich habe aktualisiert, um zu zeigen, wie ich es jetzt tun würde
maxymoo
1
@maxymoo Es ist aber immer noch eine gute Frage. Danke für das Update!
SummerEla
Ich fand das nützlich und verwandelte es in ein Paket
Oren
11

Die schnellste Methode, die ich bisher gefunden habe, ist das Erweitern des DataFrame mit .ilocund das Zurückweisen der abgeflachten Zielspalte .

Bei der üblichen Eingabe (etwas repliziert):

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

Angesichts der folgenden vorgeschlagenen Alternativen:

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

Ich finde das extend_iloc()ist das schnellste :

%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
quelle
nette Bewertung
javadba
2
Danke dafür, es hat mir wirklich geholfen. Ich habe die extens_iloc-Lösung verwendet und festgestellt, dass dies cols = [c for c in df.columns if c != col_target] sein sollte: cols = [i for i,c in enumerate(df.columns) if c != col_target] Die df.iloc[ilocations, cols].copy()Fehler, wenn sie nicht mit dem Spaltenindex dargestellt werden.
jdungan
Nochmals vielen Dank für den iloc-Vorschlag. Ich habe hier eine ausführliche Erklärung geschrieben, wie es funktioniert: medium.com/@johnadungan/… . Hoffe, es hilft jedem mit einer ähnlichen Herausforderung.
jdungan
7

Schönere alternative Lösung mit 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
quelle
Dieser erweitert Spalten, nicht Zeilen.
Oleg
@Oleg richtig, aber Sie können den DataFrame jederzeit transponieren und dann pd.Series anwenden - einfacher als die meisten anderen Vorschläge
Philipp Schwarz
7

Ähnlich wie bei der EXPLODE-Funktionalität von 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
quelle
1
Wenn ich dies ausführe, erhalte ich die folgende Fehlermeldung:NameError: global name 'copy' is not defined
frmsaul
4

Alle diese Antworten sind gut, aber ich wollte etwas wirklich Einfaches. Hier ist mein Beitrag:

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

Das war's ... benutze dies einfach, wenn du eine neue Serie willst, in der die Listen "explodiert" sind. Hier ist ein Beispiel, in dem wir value_counts () für Taco-Entscheidungen ausführen :)

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

Hier ist eine mögliche Optimierung für größere Datenrahmen. Dies läuft schneller, wenn im Feld "Explodieren" mehrere gleiche Werte vorhanden sind. (Je größer der Datenrahmen im Vergleich zur Anzahl der eindeutigen Werte im Feld ist, desto besser ist die Leistung dieses Codes.)

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

Erweiterung der .ilocAntwort von Oleg, um automatisch alle Listenspalten zu reduzieren:

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

Dies setzt voraus, dass jede Listenspalte die gleiche Listenlänge hat.

Brian Atwood
quelle
1

Anstatt apply (pd.Series) zu verwenden, können Sie die Spalte reduzieren. Dies verbessert die Leistung.

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
quelle
IndexError: Zu viele Ebenen: Index hat nur 2 Ebenen, nicht 3, wenn ich mein Beispiel versuche
vinsent paramanantham
1
Sie müssen "level" in reset_index gemäß Ihrem Beispiel
ändern