Kolom panda daftar, buat baris untuk setiap elemen daftar

163

Saya memiliki kerangka data tempat beberapa sel berisi daftar beberapa nilai. Daripada menyimpan beberapa nilai dalam sel, saya ingin memperluas kerangka data sehingga setiap item dalam daftar mendapatkan barisnya sendiri (dengan nilai yang sama di semua kolom lainnya). Jadi jika saya punya:

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

Bagaimana saya mengonversi ke bentuk panjang, misalnya:

   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 tidak penting, tidak apa-apa untuk mengatur kolom yang ada sebagai indeks dan pemesanan akhir tidak penting.

Marius
sumber
11
Dari panda 0,25 Anda juga dapat menggunakan df.explode('samples')untuk menyelesaikan ini. explodehanya dapat mendukung peledakan satu kolom untuk saat ini.
cs95

Jawaban:

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]

Hasil:

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 di sini Anda dapat menemukan solusi yang sedikit lebih umum


UPDATE: beberapa penjelasan: IMO cara termudah untuk memahami kode ini adalah dengan mencoba menjalankannya langkah demi langkah:

pada baris berikut kami mengulangi nilai dalam satu kolom Nkali di mana N- adalah panjang daftar yang sesuai:

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)

ini dapat digeneralisasi untuk semua kolom, yang berisi nilai skalar:

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]

menggunakan np.concatenate()kita dapat meratakan semua nilai di listkolom ( samples) dan mendapatkan vektor 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])

menyusun semua ini:

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]

menggunakan pd.DataFrame()[df.columns]akan menjamin bahwa kami memilih kolom dalam urutan asli ...

MaxU
sumber
3
Ini harus menjadi jawaban yang diterima. Jawaban yang saat ini diterima jauh, jauh lebih lambat dibandingkan dengan ini.
irene
1
Saya tidak tahu bagaimana cara memperbaikinya: TypeError: Tidak dapat membuang data array dari dtype ('float64') ke dtype ('int64') sesuai dengan aturan 'safe'
Greg
1
Ini adalah satu-satunya jawaban yang bekerja untuk saya, dari 10+ yang ditemukan selama satu jam penuh mencari tumpukan. Terima kasih MaxU 🙏
olisteadman
1
Perhatikan bahwa ini akan menghapus baris yang memiliki daftar kosong lst_colseluruhnya; untuk menjaga baris-baris ini dan mengisi lst_coldengan np.nan, Anda bisa melakukannya df[lst_col] = df[lst_col].apply(lambda x: x if len(x) > 0 else [np.nan])sebelum menggunakan metode ini. Jelas .masktidak akan mengembalikan daftar, karenanya .apply.
Charles Davis
Ini adalah jawaban yang sangat bagus yang harus diterima. Meskipun, itu adalah jawaban tingkat sihir hitam, dan aku, untuk satu, akan menghargai beberapa penjelasan atas apa yang sebenarnya dilakukan langkah-langkah ini.
ifly6
129

Sedikit lebih lama dari yang saya harapkan:

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

Jika Anda ingin indeks berurutan, Anda dapat menerapkannya reset_index(drop=True)pada hasilnya.

perbarui :

>>> 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
sumber
Terima kasih, bahkan langkah pertama menerapkan untuk mendapatkan setiap item di kolomnya sendiri adalah bantuan besar. Saya dapat menemukan cara yang sedikit berbeda untuk melakukannya, tetapi masih ada beberapa langkah yang adil untuk dilakukan. Tampaknya ini tidak mudah dilakukan di Panda!
Marius
1
Jawaban yang bagus Anda dapat mempersingkatnya sedikit dengan menggantinya df.apply(lambda x: pd.Series(x['samples']),axis=1)dengan df.samples.apply(pd.Series).
Dennis Golomazov
1
Catatan untuk pembaca: Ini sangat buruk karena masalah kinerja. Lihat di sini untuk solusi yang lebih performan menggunakan numpy.
cs95
2
apa solusinya ketika jumlah sampel tidak sama untuk semua baris?
SarahData
@SarahData Gunakan df.explode()seperti yang ditunjukkan di sini.
cs95
64

Panda> = 0,25

Metode Series dan DataFrame menentukan .explode()metode yang meledakkan daftar menjadi baris terpisah. Lihat bagian dokumen pada Meledak kolom seperti daftar .

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

Perhatikan bahwa ini juga menangani kolom campuran dari daftar dan skalar, serta daftar kosong dan NaN secara tepat (ini adalah kelemahan dari repeatsolusi berbasis).

Namun, Anda harus mencatat bahwa explodehanya berfungsi pada satu kolom (untuk saat ini).

PS: jika Anda ingin meledakkan kolom string , Anda harus membelah pemisah terlebih dahulu, kemudian gunakan explode. Lihat ini (sangat banyak) terkait jawaban oleh saya.

cs95
sumber
8
Akhirnya, meledak () untuk Pandas!
Kai
2
akhirnya! Mindblown! Jawaban yang bagus dari @MaxU di atas tetapi ini membuat segalanya jauh lebih sederhana.
kecanduan
12

Anda juga dapat menggunakan pd.concatdan pd.meltuntuk ini:

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

terakhir, jika perlu, Anda dapat mengurutkan berdasarkan yang pertama tiga kolom pertama.

behzad.nouri
sumber
1
Ini hanya berfungsi jika Anda tahu apriori berapa panjang daftar itu dan / atau apakah semuanya memiliki panjang yang sama?
Chill2Macht
9

Mencoba untuk bekerja melalui solusi Roman Pekar langkah demi langkah untuk memahaminya dengan lebih baik, saya datang dengan solusi saya sendiri, yang digunakan meltuntuk menghindari beberapa susun dan pengaturan ulang indeks yang membingungkan. Saya tidak bisa mengatakan bahwa itu jelas solusi yang lebih jelas:

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)

Keluaran (jelas kami dapat menjatuhkan kolom sampel asli sekarang):

                 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
sumber
6

Bagi mereka yang mencari versi jawaban Roman Pekar yang menghindari penamaan kolom manual:

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
sumber
4

Saya menemukan cara termudah adalah:

  1. Ubah sampleskolom menjadi DataFrame
  2. Bergabung dengan df asli
  3. Pencairan

Ditunjukkan di sini:

    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

Perlu dicatat bahwa ini mungkin hanya berhasil karena setiap percobaan memiliki jumlah sampel yang sama (3). Sesuatu yang lebih pintar mungkin diperlukan untuk uji coba ukuran sampel yang berbeda.

Michael Silverstein
sumber
2

Jawaban yang sangat terlambat tetapi saya ingin menambahkan ini:

Solusi cepat menggunakan vanilla Python yang juga menangani sample_numkolom dalam contoh OP. Pada dataset besar saya sendiri dengan lebih dari 10 juta baris dan hasil dengan 28 juta baris ini hanya membutuhkan waktu sekitar 38 detik. Solusi yang diterima benar-benar rusak dengan jumlah data dan mengarah memory errorpada sistem saya yang memiliki 128GB 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
sumber
2

Juga sangat terlambat, tetapi di sini ada jawaban dari Karvy1 yang bekerja dengan baik bagi saya jika Anda tidak memiliki panda> = versi 0.25: https://stackoverflow.com/a/52511166/10740287

Untuk contoh di atas, Anda dapat menulis:

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

Tes kecepatan:

%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 per loop (rata-rata ± st. Dev dari 7 berjalan, masing-masing 1000 loop)

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

4,9 ms ± 189 µs per loop (rata-rata ± st. Dev dari 7 run, masing-masing 100 loop)

%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 per loop (rata-rata ± st. Dev dari 7 run, masing-masing 1000 loop)

RĂ©my PĂ©tremand
sumber
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)

Coba ini dalam panda> = versi 0.25

Tapas
sumber
1
Tidak perlu .str.split(',')karena Pricessudah daftar.
Oren