Jak rozbić listę w komórce Dataframe na oddzielne wiersze

95

Chcę zamienić komórkę pandy zawierającą listę w wiersze dla każdej z tych wartości.

Więc weź to:

wprowadź opis obrazu tutaj

Jeśli chciałbym rozpakować i ułożyć wartości w nearest_neighborskolumnie, tak aby każda wartość była wierszem w każdym opponentindeksie, jak najlepiej się do tego zabrać? Czy istnieją metody pandy przeznaczone do takich operacji?

SpicyClubSauce
źródło
Czy mógłbyś podać przykład pożądanego wyniku i tego, czego próbowałeś do tej pory? Innym najłatwiej będzie Ci pomóc, jeśli podasz przykładowe dane, które również można wyciąć i wkleić.
dagrha,
Możesz użyć pd.DataFrame(df.nearest_neighbors.values.tolist())do rozpakowania tej kolumny, a następnie pd.mergeskleić ją z innymi.
hellpanderr
@helpanderr Nie sądzę, values.tolist()że coś tutaj robi; kolumna jest już listą
maxymoo
2
@maxymoo i.imgur.com/YGQAYOY.png
hellpanderr
1
Powiązane, ale zawierają więcej szczegółów stackoverflow.com/questions/53218931/ ...
BEN_YO

Odpowiedzi:

56

W poniższym kodzie najpierw zresetowałem indeks, aby ułatwić iterację wiersza.

Tworzę listę list, w których każdy element listy zewnętrznej jest wierszem docelowej ramki DataFrame, a każdy element listy wewnętrznej jest jedną z kolumn. Ta zagnieżdżona lista zostanie ostatecznie połączona w celu utworzenia żądanej ramki DataFrame.

Używam lambdafunkcji wraz z iteracją listy, aby utworzyć wiersz dla każdego elementu nearest_neighborssparowanego z odpowiednim namei opponent.

Na koniec tworzę nowy DataFrame z tej listy (używając oryginalnych nazw kolumn i ustawiając indeks z powrotem na namei 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

EDYCJA CZERWIEC 2017

Alternatywna metoda jest następująca:

>>> (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
źródło
apply(pd.Series)sprawdza się w najmniejszych ramkach, ale w przypadku ramek o rozsądnych rozmiarach należy ponownie rozważyć bardziej wydajne rozwiązanie. Zobacz Kiedy powinienem używać pandy Apply () w moim kodzie? (Lepszym rozwiązaniem jest najpierw wymienienie kolumny).
cs95
2
Rozbijanie kolumny podobnej do listy zostało znacznie uproszczone w pandach 0,25 dzięki dodaniu explode()metody. Dodałem odpowiedź z przykładem używając tej samej konfiguracji df jak tutaj.
joelostblom
@joelostblom Dobrze słyszeć. Dziękujemy za dodanie przykładu z bieżącym użyciem.
Alexander,
37
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')

Na zewnątrz:

                    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
źródło
2
Zauważ, że działa to tylko dla pojedynczej kolumny (od 0.25). Zobacz tutaj i tutaj, aby uzyskać bardziej ogólne rozwiązania.
cs95
jest to najłatwiejsze i najszybsze rozwiązanie (jeśli masz tylko jedną kolumnę z listą do eksplozji lub „odprężenia”, jak by to nazwał w mongodb)
annakeuchenius
Najszybsze rozwiązanie od pandas docu. Ale uważaj: .explode nie jest na miejscu! Raczej takdf = df.explode(...)
harmonijka141
34

Użyj apply(pd.Series)i stack, a następnie reset_indexito_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

Detale

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]
Zero
źródło
1
Pokochaj elegancję swojego rozwiązania! Czy przez przypadek porównałeś to z innymi podejściami?
rpyzh
1
Wynik df.nearest_neighbors.apply(pd.Series)jest dla mnie bardzo zdumiewający;
Calum You
1
@rpyzh Tak, jest dość elegancki, ale żałośnie powolny.
cs95
16

Myślę, że to naprawdę dobre pytanie, w Hive byś użył EXPLODE, myślę, że jest argument, że Pandy powinny domyślnie zawierać tę funkcję. Prawdopodobnie rozbiłbym kolumnę listy za pomocą zagnieżdżonego rozumienia generatora w następujący sposób:

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
źródło
Podoba mi się, jak to rozwiązanie pozwala na różną liczbę pozycji listy w każdym wierszu.
user1718097
Czy istnieje sposób na zachowanie oryginalnego indeksu za pomocą tej metody?
SummerEla
2
@SummerEla lol to była naprawdę stara odpowiedź, zaktualizowałem, aby pokazać, jak bym to zrobił teraz
maxymoo
1
@maxymoo To wciąż świetne pytanie. Dzięki za aktualizację!
SummerEla
Znalazłem to przydatne i przekształciłem je w pakiet
Oren
11

Najszybszy sposób, że do tej pory znaleziono rozszerza DataFrame z .iloci przypisanie powrotem spłaszczony kolumny docelowej.

Biorąc pod uwagę zwykłe dane wejściowe (trochę replikowane):

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

Biorąc pod uwagę następujące sugerowane alternatywy:

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

Uważam, że extend_iloc()jest to najszybsze :

%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
źródło
niezła ocena
javadba
2
Dzięki za to, naprawdę mi pomogło. Użyłem rozwiązanie extend_iloc i okazało się, że cols = [c for c in df.columns if c != col_target] powinno być: cols = [i for i,c in enumerate(df.columns) if c != col_target] The df.iloc[ilocations, cols].copy()błędy, jeśli nie przedstawiane z indeksu kolumny.
jdungan
Jeszcze raz dziękuję za sugestię iloc. Napisałem szczegółowe wyjaśnienie, jak to działa tutaj: medium.com/@johnadungan/… . Mam nadzieję, że pomoże to każdemu z podobnym wyzwaniem.
jdungan
7

Ładniejsze rozwiązanie alternatywne z aplikacją (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
źródło
Ten rozwija kolumny, a nie wiersze.
Oleg
@Oleg racja, ale zawsze możesz przetransponować DataFrame, a następnie zastosować pd.Series - tak prostsze niż większość innych sugestii
Philipp Schwarz
7

Podobna do funkcji WYBUCHANIA 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
źródło
1
Kiedy to uruchamiam, NameError: global name 'copy' is not defined
pojawia się
4

Więc wszystkie te odpowiedzi są dobre, ale chciałem czegoś ^ naprawdę prostego ^ więc oto mój wkład:

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

To wszystko… po prostu użyj tego, gdy chcesz nową serię, w której listy są „eksplodowane”. Oto przykład, w którym wykonujemy value_counts () na wybór 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
Briford Wylie
źródło
2

Oto potencjalna optymalizacja dla większych ramek danych. Działa to szybciej, gdy w polu „eksplodującym” jest kilka równych wartości. (Im większa ramka danych jest porównywana z liczbą unikatowych wartości w polu, tym lepszy będzie ten kod).

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
źródło
1

Rozszerzanie .ilocodpowiedzi Olega, aby automatycznie spłaszczyć wszystkie kolumny list:

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

Zakłada się, że każda kolumna listy ma taką samą długość listy.

Brian Atwood
źródło
1

Zamiast używać apply (pd.Series), możesz spłaszczyć kolumnę. Poprawia to wydajność.

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
źródło
IndexError: Zbyt wiele poziomów: Indeks ma tylko 2 poziomy, a nie 3, kiedy próbuję mojego przykładu
vinsent paramanantham
1
Musisz zmienić "level" w reset_index zgodnie z twoim przykładem
suleep kumar