Kolumna Pandy z listami, utwórz wiersz dla każdego elementu listy

163

Mam ramkę danych, w której niektóre komórki zawierają listy wielu wartości. Zamiast przechowywać wiele wartości w komórce, chciałbym rozszerzyć ramkę danych, aby każdy element na liście miał swój własny wiersz (z tymi samymi wartościami we wszystkich innych kolumnach). Więc jeśli mam:

import pandas as pd
import numpy as np

df = pd.DataFrame(
    {'trial_num': [1, 2, 3, 1, 2, 3],
     'subject': [1, 1, 1, 2, 2, 2],
     'samples': [list(np.random.randn(3).round(2)) for i in range(6)]
    }
)

df
Out[10]: 
                 samples  subject  trial_num
0    [0.57, -0.83, 1.44]        1          1
1    [-0.01, 1.13, 0.36]        1          2
2   [1.18, -1.46, -0.94]        1          3
3  [-0.08, -4.22, -2.05]        2          1
4     [0.72, 0.79, 0.53]        2          2
5    [0.4, -0.32, -0.13]        2          3

Jak przekonwertować na długą formę, np .:

   subject  trial_num  sample  sample_num
0        1          1    0.57           0
1        1          1   -0.83           1
2        1          1    1.44           2
3        1          2   -0.01           0
4        1          2    1.13           1
5        1          2    0.36           2
6        1          3    1.18           0
# etc.

Indeks nie jest ważny, można ustawić istniejące kolumny jako indeks, a ostateczna kolejność nie jest ważna.

Marius
źródło
11
Od pandy 0,25 możesz również użyć df.explode('samples')do rozwiązania tego problemu. explodena razie obsługuje tylko rozbicie jednej kolumny.
cs95

Odpowiedzi:

48
lst_col = 'samples'

r = pd.DataFrame({
      col:np.repeat(df[col].values, df[lst_col].str.len())
      for col in df.columns.drop(lst_col)}
    ).assign(**{lst_col:np.concatenate(df[lst_col].values)})[df.columns]

Wynik:

In [103]: r
Out[103]:
    samples  subject  trial_num
0      0.10        1          1
1     -0.20        1          1
2      0.05        1          1
3      0.25        1          2
4      1.32        1          2
5     -0.17        1          2
6      0.64        1          3
7     -0.22        1          3
8     -0.71        1          3
9     -0.03        2          1
10    -0.65        2          1
11     0.76        2          1
12     1.77        2          2
13     0.89        2          2
14     0.65        2          2
15    -0.98        2          3
16     0.65        2          3
17    -0.30        2          3

PS tutaj możesz znaleźć nieco bardziej ogólne rozwiązanie


UPDATE: kilka wyjaśnień: IMO Najłatwiejszym sposobem zrozumienia tego kodu jest wykonanie go krok po kroku:

w kolejnym wierszu powtarzamy wartości w jednej kolumnie Nrazy, gdzie N- jest długością odpowiedniej listy:

In [10]: np.repeat(df['trial_num'].values, df[lst_col].str.len())
Out[10]: array([1, 1, 1, 2, 2, 2, 3, 3, 3, 1, 1, 1, 2, 2, 2, 3, 3, 3], dtype=int64)

można to uogólnić dla wszystkich kolumn zawierających wartości skalarne:

In [11]: pd.DataFrame({
    ...:           col:np.repeat(df[col].values, df[lst_col].str.len())
    ...:           for col in df.columns.drop(lst_col)}
    ...:         )
Out[11]:
    trial_num  subject
0           1        1
1           1        1
2           1        1
3           2        1
4           2        1
5           2        1
6           3        1
..        ...      ...
11          1        2
12          2        2
13          2        2
14          2        2
15          3        2
16          3        2
17          3        2

[18 rows x 2 columns]

używając np.concatenate()możemy spłaszczyć wszystkie wartości w listkolumnie ( samples) i otrzymać wektor 1D:

In [12]: np.concatenate(df[lst_col].values)
Out[12]: array([-1.04, -0.58, -1.32,  0.82, -0.59, -0.34,  0.25,  2.09,  0.12,  0.83, -0.88,  0.68,  0.55, -0.56,  0.65, -0.04,  0.36, -0.31])

składając to wszystko razem:

In [13]: pd.DataFrame({
    ...:           col:np.repeat(df[col].values, df[lst_col].str.len())
    ...:           for col in df.columns.drop(lst_col)}
    ...:         ).assign(**{lst_col:np.concatenate(df[lst_col].values)})
Out[13]:
    trial_num  subject  samples
0           1        1    -1.04
1           1        1    -0.58
2           1        1    -1.32
3           2        1     0.82
4           2        1    -0.59
5           2        1    -0.34
6           3        1     0.25
..        ...      ...      ...
11          1        2     0.68
12          2        2     0.55
13          2        2    -0.56
14          2        2     0.65
15          3        2    -0.04
16          3        2     0.36
17          3        2    -0.31

[18 rows x 3 columns]

użycie pd.DataFrame()[df.columns]zagwarantuje, że wybieramy kolumny w oryginalnej kolejności ...

MaxU
źródło
3
To powinna być akceptowana odpowiedź. Obecnie akceptowana odpowiedź jest dużo, dużo wolniejsza w porównaniu z tym.
Irene
1
Nie mogę dowiedzieć się, jak to naprawić: TypeError: Nie można rzutować danych tablicy z dtype ('float64') na dtype ('int64') zgodnie z regułą 'safe'
Greg
1
To jedyna odpowiedź, która mi pomogła, spośród ponad 10 znalezionych podczas pełnej godziny przeszukiwania stosów. Dzięki MaxU 🙏
olisteadman
1
Zauważ, że to lst_colcałkowicie usuwa wiersze, które mają pustą listę ; aby zachować te wiersze i wypełnianie ich lst_colpomocą np.nan, można po prostu zrobić df[lst_col] = df[lst_col].apply(lambda x: x if len(x) > 0 else [np.nan])przed użyciem tej metody. Najwyraźniej .masknie zwróci list, stąd rozszerzenie .apply.
Charles Davis,
To doskonała odpowiedź, którą należy zaakceptować. Chociaż jest to odpowiedź na poziomie czarnej magii, a ja, na przykład, byłbym wdzięczny za wyjaśnienie, co w rzeczywistości robią te kroki.
ifly 6
129

Nieco dłużej niż się spodziewałem:

>>> df
                samples  subject  trial_num
0  [-0.07, -2.9, -2.44]        1          1
1   [-1.52, -0.35, 0.1]        1          2
2  [-0.17, 0.57, -0.65]        1          3
3  [-0.82, -1.06, 0.47]        2          1
4   [0.79, 1.35, -0.09]        2          2
5   [1.17, 1.14, -1.79]        2          3
>>>
>>> s = df.apply(lambda x: pd.Series(x['samples']),axis=1).stack().reset_index(level=1, drop=True)
>>> s.name = 'sample'
>>>
>>> df.drop('samples', axis=1).join(s)
   subject  trial_num  sample
0        1          1   -0.07
0        1          1   -2.90
0        1          1   -2.44
1        1          2   -1.52
1        1          2   -0.35
1        1          2    0.10
2        1          3   -0.17
2        1          3    0.57
2        1          3   -0.65
3        2          1   -0.82
3        2          1   -1.06
3        2          1    0.47
4        2          2    0.79
4        2          2    1.35
4        2          2   -0.09
5        2          3    1.17
5        2          3    1.14
5        2          3   -1.79

Jeśli chcesz indeksowania sekwencyjnego, możesz zastosować reset_index(drop=True)do wyniku.

aktualizacja :

>>> res = df.set_index(['subject', 'trial_num'])['samples'].apply(pd.Series).stack()
>>> res = res.reset_index()
>>> res.columns = ['subject','trial_num','sample_num','sample']
>>> res
    subject  trial_num  sample_num  sample
0         1          1           0    1.89
1         1          1           1   -2.92
2         1          1           2    0.34
3         1          2           0    0.85
4         1          2           1    0.24
5         1          2           2    0.72
6         1          3           0   -0.96
7         1          3           1   -2.72
8         1          3           2   -0.11
9         2          1           0   -1.33
10        2          1           1    3.13
11        2          1           2   -0.65
12        2          2           0    0.10
13        2          2           1    0.65
14        2          2           2    0.15
15        2          3           0    0.64
16        2          3           1   -0.10
17        2          3           2   -0.76
Roman Pekar
źródło
Dzięki, nawet pierwszy krok ubiegania się o umieszczenie każdej pozycji w osobnej kolumnie jest ogromną pomocą. Udało mi się wymyślić nieco inny sposób, aby to zrobić, ale nadal wymaga to kilku kroków. Najwyraźniej nie jest to łatwe do zrobienia na Pandach!
Marius
1
Świetna odpowiedź. Możesz go nieco skrócić, zastępując df.apply(lambda x: pd.Series(x['samples']),axis=1)go df.samples.apply(pd.Series).
Dennis Golomazov
1
Uwaga dla czytelników: to bardzo cierpi z powodu problemów z wydajnością. Zobacz tutaj, aby uzyskać znacznie bardziej wydajne rozwiązanie przy użyciu numpy.
cs95
2
jakie jest rozwiązanie, gdy liczba próbek nie jest taka sama dla wszystkich rzędów?
SarahData,
@SarahData Użyj, df.explode()jak pokazano tutaj.
cs95
63

Pandy> = 0,25

Metody Series i DataFrame definiują .explode()metodę, która rozbija listy na oddzielne wiersze. Zobacz sekcję Dokumenty na temat Rozbijanie kolumny podobnej do listy .

df = pd.DataFrame({
    'var1': [['a', 'b', 'c'], ['d', 'e',], [], np.nan], 
    'var2': [1, 2, 3, 4]
})
df
        var1  var2
0  [a, b, c]     1
1     [d, e]     2
2         []     3
3        NaN     4

df.explode('var1')

  var1  var2
0    a     1
0    b     1
0    c     1
1    d     2
1    e     2
2  NaN     3  # empty list converted to NaN
3  NaN     4  # NaN entry preserved as-is

# to reset the index to be monotonically increasing...
df.explode('var1').reset_index(drop=True)

  var1  var2
0    a     1
1    b     1
2    c     1
3    d     2
4    e     2
5  NaN     3
6  NaN     4

Zauważ, że obsługuje to również odpowiednio mieszane kolumny list i skalarów, a także puste listy i NaN (jest to wada repeat rozwiązań opartych na ).

Jednak powinieneś to zauważyć explode działa tylko na jednej kolumnie (na razie).

PS: jeśli chcesz rozbić kolumnę ciągów , musisz najpierw podzielić na separator, a następnie użyć explode. Zobacz tę (bardzo) powiązaną odpowiedź przeze mnie.

cs95
źródło
8
Wreszcie, eksploduj () dla Pand!
Kai
2
Wreszcie! Mindblown! Świetna odpowiedź od @MaxU powyżej, ale to znacznie upraszcza sprawę.
uzależniony
12

możesz również użyć pd.concati pd.meltdo tego:

>>> objs = [df, pd.DataFrame(df['samples'].tolist())]
>>> pd.concat(objs, axis=1).drop('samples', axis=1)
   subject  trial_num     0     1     2
0        1          1 -0.49 -1.00  0.44
1        1          2 -0.28  1.48  2.01
2        1          3 -0.52 -1.84  0.02
3        2          1  1.23 -1.36 -1.06
4        2          2  0.54  0.18  0.51
5        2          3 -2.18 -0.13 -1.35
>>> pd.melt(_, var_name='sample_num', value_name='sample', 
...         value_vars=[0, 1, 2], id_vars=['subject', 'trial_num'])
    subject  trial_num sample_num  sample
0         1          1          0   -0.49
1         1          2          0   -0.28
2         1          3          0   -0.52
3         2          1          0    1.23
4         2          2          0    0.54
5         2          3          0   -2.18
6         1          1          1   -1.00
7         1          2          1    1.48
8         1          3          1   -1.84
9         2          1          1   -1.36
10        2          2          1    0.18
11        2          3          1   -0.13
12        1          1          2    0.44
13        1          2          2    2.01
14        1          3          2    0.02
15        2          1          2   -1.06
16        2          2          2    0.51
17        2          3          2   -1.35

na koniec, jeśli potrzebujesz, możesz posortować według pierwszych trzech kolumn.

behzad.nouri
źródło
1
Działa to tylko wtedy, gdy wiesz a priori, jaka będzie długość list i / lub czy wszystkie będą miały taką samą długość?
Chill2Macht
9

Próbując krok po kroku przeanalizować rozwiązanie Romana Pekara, aby lepiej je zrozumieć, wymyśliłem własne rozwiązanie, które meltpozwala uniknąć niektórych nieporozumień związanych z układaniem w stosy i resetowaniem indeksów. Nie mogę jednak powiedzieć, że jest to oczywiście jaśniejsze rozwiązanie:

items_as_cols = df.apply(lambda x: pd.Series(x['samples']), axis=1)
# Keep original df index as a column so it's retained after melt
items_as_cols['orig_index'] = items_as_cols.index

melted_items = pd.melt(items_as_cols, id_vars='orig_index', 
                       var_name='sample_num', value_name='sample')
melted_items.set_index('orig_index', inplace=True)

df.merge(melted_items, left_index=True, right_index=True)

Wyjście (oczywiście możemy teraz usunąć oryginalną kolumnę próbek):

                 samples  subject  trial_num sample_num  sample
0    [1.84, 1.05, -0.66]        1          1          0    1.84
0    [1.84, 1.05, -0.66]        1          1          1    1.05
0    [1.84, 1.05, -0.66]        1          1          2   -0.66
1    [-0.24, -0.9, 0.65]        1          2          0   -0.24
1    [-0.24, -0.9, 0.65]        1          2          1   -0.90
1    [-0.24, -0.9, 0.65]        1          2          2    0.65
2    [1.15, -0.87, -1.1]        1          3          0    1.15
2    [1.15, -0.87, -1.1]        1          3          1   -0.87
2    [1.15, -0.87, -1.1]        1          3          2   -1.10
3   [-0.8, -0.62, -0.68]        2          1          0   -0.80
3   [-0.8, -0.62, -0.68]        2          1          1   -0.62
3   [-0.8, -0.62, -0.68]        2          1          2   -0.68
4    [0.91, -0.47, 1.43]        2          2          0    0.91
4    [0.91, -0.47, 1.43]        2          2          1   -0.47
4    [0.91, -0.47, 1.43]        2          2          2    1.43
5  [-1.14, -0.24, -0.91]        2          3          0   -1.14
5  [-1.14, -0.24, -0.91]        2          3          1   -0.24
5  [-1.14, -0.24, -0.91]        2          3          2   -0.91
Marius
źródło
6

Dla tych, którzy szukają wersji odpowiedzi Romana Pekara, która unika ręcznego nazewnictwa kolumn:

column_to_explode = 'samples'
res = (df
       .set_index([x for x in df.columns if x != column_to_explode])[column_to_explode]
       .apply(pd.Series)
       .stack()
       .reset_index())
res = res.rename(columns={
          res.columns[-2]:'exploded_{}_index'.format(column_to_explode),
          res.columns[-1]: '{}_exploded'.format(column_to_explode)})
Charles Davis
źródło
4

Najprościej było:

  1. Przekonwertuj sampleskolumnę na DataFrame
  2. Łączenie z oryginalnym df
  3. Topienie

Pokazano tutaj:

    df.samples.apply(lambda x: pd.Series(x)).join(df).\
melt(['subject','trial_num'],[0,1,2],var_name='sample')

        subject  trial_num sample  value
    0         1          1      0  -0.24
    1         1          2      0   0.14
    2         1          3      0  -0.67
    3         2          1      0  -1.52
    4         2          2      0  -0.00
    5         2          3      0  -1.73
    6         1          1      1  -0.70
    7         1          2      1  -0.70
    8         1          3      1  -0.29
    9         2          1      1  -0.70
    10        2          2      1  -0.72
    11        2          3      1   1.30
    12        1          1      2  -0.55
    13        1          2      2   0.10
    14        1          3      2  -0.44
    15        2          1      2   0.13
    16        2          2      2  -1.44
    17        2          3      2   0.73

Warto zauważyć, że mogło to zadziałać tylko dlatego, że każda próba ma taką samą liczbę próbek (3). W przypadku prób z różnymi rozmiarami próbek może być konieczne coś mądrzejszego.

Michael Silverstein
źródło
2

Bardzo późna odpowiedź, ale chcę to dodać:

Szybkie rozwiązanie wykorzystujące waniliowy Python, który obsługuje również sample_numkolumnę w przykładzie OP. W moim dużym zbiorze danych z ponad 10 milionami wierszy i wynikiem z 28 milionami wierszy zajmuje to tylko około 38 sekund. Przyjęte rozwiązanie całkowicie psuje się przy takiej ilości danych i prowadzi do memory errormojego systemu, który ma 128 GB pamięci RAM.

df = df.reset_index(drop=True)
lstcol = df.lstcol.values
lstcollist = []
indexlist = []
countlist = []
for ii in range(len(lstcol)):
    lstcollist.extend(lstcol[ii])
    indexlist.extend([ii]*len(lstcol[ii]))
    countlist.extend([jj for jj in range(len(lstcol[ii]))])
df = pd.merge(df.drop("lstcol",axis=1),pd.DataFrame({"lstcol":lstcollist,"lstcol_num":countlist},
index=indexlist),left_index=True,right_index=True).reset_index(drop=True)
Khris
źródło
2

Również bardzo późno, ale tutaj jest odpowiedź od Karvy1, która zadziałała dobrze, jeśli nie masz wersji pandy> = 0.25: https://stackoverflow.com/a/52511166/10740287

Dla przykładu powyżej możesz napisać:

data = [(row.subject, row.trial_num, sample) for row in df.itertuples() for sample in row.samples]
data = pd.DataFrame(data, columns=['subject', 'trial_num', 'samples'])

Test prędkości:

%timeit data = pd.DataFrame([(row.subject, row.trial_num, sample) for row in df.itertuples() for sample in row.samples], columns=['subject', 'trial_num', 'samples'])

1,33 ms ± 74,8 μs na pętlę (średnia ± odchylenie standardowe z 7 przebiegów, po 1000 pętli każda)

%timeit data = df.set_index(['subject', 'trial_num'])['samples'].apply(pd.Series).stack().reset_index()

4,9 ms ± 189 μs na pętlę (średnia ± odchylenie standardowe 7 przebiegów, po 100 pętli każda)

%timeit data = pd.DataFrame({col:np.repeat(df[col].values, df['samples'].str.len())for col in df.columns.drop('samples')}).assign(**{'samples':np.concatenate(df['samples'].values)})

1,38 ms ± 25 μs na pętlę (średnia ± odchylenie standardowe z 7 przebiegów, po 1000 pętli każda)

Rémy Pétremand
źródło
1
import pandas as pd
df = pd.DataFrame([{'Product': 'Coke', 'Prices': [100,123,101,105,99,94,98]},{'Product': 'Pepsi', 'Prices': [101,104,104,101,99,99,99]}])
print(df)
df = df.assign(Prices=df.Prices.str.split(',')).explode('Prices')
print(df)

Spróbuj tego w pandach> = 0.25 wersji

Tapas
źródło
1
Nie ma potrzeby, .str.split(',')ponieważ Pricesjest to już lista.
Oren