Cột Pandas của danh sách, tạo một hàng cho mỗi thành phần danh sách


163

Tôi có một khung dữ liệu trong đó một số ô chứa danh sách nhiều giá trị. Thay vì lưu trữ nhiều giá trị trong một ô, tôi muốn mở rộng khung dữ liệu để mỗi mục trong danh sách có hàng riêng (có cùng giá trị trong tất cả các cột khác). Vì vậy, nếu tôi có:

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

Làm cách nào để chuyển đổi thành dạng dài, ví dụ:

   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.

Chỉ mục không quan trọng, bạn có thể đặt các cột hiện tại làm chỉ mục và thứ tự cuối cùng không quan trọng.


11
Từ gấu trúc 0,25 bạn cũng có thể sử dụng df.explode('samples')để giải quyết điều này. explodebây giờ chỉ có thể hỗ trợ phát nổ một cột.
cs95

Câu trả lời:


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]

Kết quả:

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 ở đây bạn có thể tìm thấy một giải pháp chung chung hơn một chút


CẬP NHẬT: một số giải thích: IMO cách dễ nhất để hiểu mã này là cố gắng thực hiện từng bước:

trong dòng sau chúng ta sẽ lặp lại các giá trị trong một Nlần cột trong đó N- là độ dài của danh sách tương ứng:

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)

điều này có thể được tổng quát hóa cho tất cả các cột, chứa các giá trị vô hướng:

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]

bằng cách sử dụng, np.concatenate()chúng ta có thể làm phẳng tất cả các giá trị trong listcột ( samples) và nhận được vectơ 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])

đặt tất cả những thứ này lại với nhau:

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]

sử dụng pd.DataFrame()[df.columns]sẽ đảm bảo rằng chúng tôi đang chọn các cột theo thứ tự ban đầu ...


3
Đây phải là câu trả lời được chấp nhận. Câu trả lời hiện được chấp nhận là nhiều, chậm hơn nhiều so với điều này.
irene

1
Tôi không thể tìm ra cách khắc phục điều này: TypeError: Không thể truyền dữ liệu mảng từ dtype ('float64') sang dtype ('int64') theo quy tắc 'an toàn'
Greg

1
Đây là câu trả lời duy nhất có hiệu quả với tôi, trong số hơn 10 người được tìm thấy trong suốt một giờ tìm kiếm các Ngăn xếp. Cảm ơn MaxU
olisteadman

1
Lưu ý rằng điều này giảm các hàng có danh sách trống lst_colhoàn toàn; để giữ cho các hàng và cư của họ lst_colvới np.nan, bạn chỉ có thể làm df[lst_col] = df[lst_col].apply(lambda x: x if len(x) > 0 else [np.nan])trước khi sử dụng phương pháp này. Rõ ràng là .masksẽ không trả lại danh sách, do đó .apply.
Charles Davis

Đây là một câu trả lời tuyệt vời nên được chấp nhận. Mặc dù, đó là một câu trả lời cấp độ ma thuật đen, và tôi, với một người, sẽ đánh giá cao một số lời giải thích cho những gì các bước này trong thực tế làm.
ifly6

129

Một chút lâu hơn tôi mong đợi:

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

Nếu bạn muốn chỉ số tuần tự, bạn có thể áp dụng reset_index(drop=True)cho kết quả.

cập nhật :

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

Cảm ơn, ngay cả bước đầu tiên của việc áp dụng để có được từng mục trong cột riêng của nó là một sự trợ giúp rất lớn. Tôi đã có thể đưa ra một cách hơi khác để làm điều đó, nhưng vẫn còn một vài bước công bằng liên quan. Rõ ràng điều này không đơn giản để làm trong Pandas!
Marius

1
Câu trả lời chính xác. Bạn có thể rút ngắn nó một chút bằng cách thay thế df.apply(lambda x: pd.Series(x['samples']),axis=1)bằng df.samples.apply(pd.Series).
Dennis Golomazov

1
Lưu ý cho độc giả: Điều này chịu đựng khủng khiếp từ các vấn đề hiệu suất. Xem ở đây để có một giải pháp hiệu quả hơn nhiều bằng cách sử dụng numpy.
cs95

2
Giải pháp nào khi số lượng mẫu không giống nhau cho tất cả các hàng?
SarahData

@SarahData Sử dụng df.explode()như hiển thị ở đây.
cs95

63

Gấu trúc> = 0,25

Các phương thức sê-ri và DataFrame xác định một .explode()phương thức khám phá danh sách thành các hàng riêng biệt. Xem phần tài liệu về Phát nổ cột giống như danh sách .

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

Lưu ý rằng điều này cũng xử lý các cột hỗn hợp của danh sách và vô hướng, cũng như danh sách trống và NaN thích hợp (đây là một nhược điểm của repeat giải pháp dựa trên cơ sở).

Tuy nhiên, bạn cần lưu ý rằng explodechỉ hoạt động trên một cột duy nhất (hiện tại).

PS: nếu bạn đang muốn nổ tung một chuỗi các chuỗi , trước tiên bạn cần phải phân tách trên một dấu phân cách, sau đó sử dụng explode. Xem câu trả lời này (rất nhiều) liên quan của tôi.


8
Cuối cùng, một vụ nổ () cho Pandas!
Kai

2
cuối cùng Tâm trí! Câu trả lời tuyệt vời từ @MaxU ở trên nhưng điều này làm cho mọi thứ đơn giản hơn nhiều.
nghiện

12

bạn cũng có thể sử dụng pd.concatpd.meltcho việc này:

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

cuối cùng, nếu bạn cần, bạn có thể sắp xếp căn cứ vào ba cột đầu tiên.


1
Điều này chỉ hoạt động nếu bạn biết một tiên nghiệm độ dài của danh sách sẽ là gì và / hoặc nếu tất cả chúng sẽ có cùng độ dài?
Chill2Macht

9

Cố gắng từng bước thực hiện giải pháp của Roman Pekar để hiểu rõ hơn về nó, tôi đã đưa ra giải pháp của riêng mình, phương pháp này meltđể tránh một số xếp chồng khó hiểu và đặt lại chỉ mục. Tôi không thể nói rằng đó rõ ràng là một giải pháp rõ ràng hơn:

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)

Đầu ra (rõ ràng chúng ta có thể bỏ cột mẫu ban đầu ngay bây giờ):

                 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

6

Đối với những người tìm kiếm một phiên bản của câu trả lời của Roman Pekar tránh việc đặt tên cột thủ công:

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

4

Tôi tìm thấy cách dễ nhất là:

  1. Chuyển đổi samples cột thành DataFrame
  2. Tham gia với df gốc
  3. Nóng chảy

Thể hiện ở đây:

    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

Điều đáng chú ý là điều này có thể chỉ hoạt động vì mỗi thử nghiệm có cùng số lượng mẫu (3). Một cái gì đó thông minh hơn có thể cần thiết cho các thử nghiệm ở các cỡ mẫu khác nhau.


2

Câu trả lời rất muộn nhưng tôi muốn thêm điều này:

Một giải pháp nhanh bằng cách sử dụng vani Python cũng chăm sóc sample_numcột trong ví dụ của OP. Trên tập dữ liệu lớn của riêng tôi với hơn 10 triệu hàng và kết quả với 28 triệu hàng, việc này chỉ mất khoảng 38 giây. Giải pháp được chấp nhận hoàn toàn phá vỡ với lượng dữ liệu đó và dẫn đến memory errorhệ thống của tôi có 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)

2

Cũng rất muộn, nhưng đây là câu trả lời từ Karvy1 hoạt động tốt với tôi nếu bạn không có gấu trúc> = 0,25 phiên bản: https://stackoverflow.com/a/52511166/10740287

Đối với ví dụ trên bạn có thể viết:

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

Kiểm tra tốc độ:

%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 Trao đổi trên mỗi vòng lặp (trung bình ± std. Dev của 7 lần chạy, mỗi vòng 1000 vòng)

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

4,9 ms ± 189 Nhận mỗi vòng lặp (trung bình ± std. Dev của 7 lần chạy, mỗi vòng 100 lần)

%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 Lời nói trên mỗi vòng lặp (trung bình ± std. Dev của 7 lần chạy, mỗi vòng 1000 vòng)


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)

Hãy thử điều này trong gấu trúc> = 0,25 phiên bản


1
Không cần .str.split(',')Pricesđã là một danh sách.
Oren
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.