Áp dụng chức năng gấu trúc cho cột để tạo nhiều cột mới?


215

Làm thế nào để làm điều này trong gấu trúc:

Tôi có một chức năng extract_text_featurestrên một cột văn bản, trả về nhiều cột đầu ra. Cụ thể, hàm trả về 6 giá trị.

Hàm này hoạt động, tuy nhiên dường như không có bất kỳ loại trả về thích hợp nào (danh sách DataFrame / mảng numpy / danh sách Python) sao cho đầu ra có thể được gán chính xác df.ix[: ,10:16] = df.textcol.map(extract_text_features)

Vì vậy, tôi nghĩ rằng tôi cần phải quay trở lại để lặp đi lặp lại df.iterrows(), theo điều này ?

CẬP NHẬT: Lặp lại với df.iterrows()tốc độ chậm hơn ít nhất 20 lần, vì vậy tôi đã đầu hàng và tách chức năng thành sáu .map(lambda ...)cuộc gọi riêng biệt .

CẬP NHẬT 2: câu hỏi này đã được hỏi lại vào khoảng v0.11.0 . Do đó phần lớn câu hỏi và câu trả lời không quá liên quan.


1
Tôi không nghĩ bạn có thể thực hiện nhiều bài tập theo cách bạn đã viết : df.ix[: ,10:16]. Tôi nghĩ bạn sẽ phải mergetính năng của bạn vào bộ dữ liệu.
Zelazny7

1
Đối với những người muốn một giải pháp hiệu quả hơn nhiều, hãy kiểm tra giải pháp này dưới đây không sử dụngapply
Ted Petrou

Hầu hết các phép toán số với gấu trúc có thể được vector hóa - điều này có nghĩa là chúng nhanh hơn nhiều so với phép lặp thông thường. OTOH, một số hoạt động (như chuỗi và regex) vốn đã khó để vector hóa. Trong trường hợp này, điều quan trọng là phải hiểu làm thế nào để lặp lại dữ liệu của bạn. Thông tin thêm về thời điểm và cách lặp lại dữ liệu của bạn sẽ được thực hiện, vui lòng đọc Đối với các vòng lặp với Pandas - Khi nào tôi nên quan tâm? .
cs95

@coldspeed: vấn đề chính không phải là lựa chọn hiệu năng cao hơn trong số một số tùy chọn, đó là chiến đấu với cú pháp gấu trúc để làm cho điều này hoạt động hoàn toàn, trở lại khoảng v0.11.0 .
smci

Thật vậy, bình luận dành cho những độc giả tương lai đang tìm kiếm các giải pháp lặp lại, những người không biết gì hơn hoặc biết họ đang làm gì.
cs95

Câu trả lời:


109

Dựa trên câu trả lời của người dùng1827356, bạn có thể thực hiện bài tập trong một lượt bằng cách sử dụng df.merge:

df.merge(df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1})), 
    left_index=True, right_index=True)

    textcol  feature1  feature2
0  0.772692  1.772692 -0.227308
1  0.857210  1.857210 -0.142790
2  0.065639  1.065639 -0.934361
3  0.819160  1.819160 -0.180840
4  0.088212  1.088212 -0.911788

EDIT: Xin lưu ý về mức tiêu thụ bộ nhớ lớn và tốc độ thấp: https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ !


2
Chỉ vì tò mò, nó có dự kiến ​​sẽ sử dụng nhiều bộ nhớ bằng cách này không? Tôi đang làm điều này trên một khung dữ liệu chứa 2,5 triệu hàng và tôi gần như gặp vấn đề về bộ nhớ (cũng chậm hơn nhiều so với việc chỉ trả về 1 cột).
Jeffrey04

2
'df.join (df.textcol.apply (lambda s: pd.Series ({' Feature1 ': s + 1,' Feature2 ': s-1})))' sẽ là một lựa chọn tốt hơn tôi nghĩ.
Shivam K. Thakkar

@ShivamKThakkar tại sao bạn nghĩ đề xuất của bạn sẽ là một lựa chọn tốt hơn? Nó sẽ hiệu quả hơn bạn nghĩ hoặc có chi phí bộ nhớ ít hơn?
tsando

1
Vui lòng xem xét tốc độ và bộ nhớ cần có: ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42

189

Tôi thường làm điều này bằng cách sử dụng zip:

>>> df = pd.DataFrame([[i] for i in range(10)], columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5
6    6
7    7
8    8
9    9

>>> def powers(x):
>>>     return x, x**2, x**3, x**4, x**5, x**6

>>> df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
>>>     zip(*df['num'].map(powers))

>>> df
        num     p1      p2      p3      p4      p5      p6
0       0       0       0       0       0       0       0
1       1       1       1       1       1       1       1
2       2       2       4       8       16      32      64
3       3       3       9       27      81      243     729
4       4       4       16      64      256     1024    4096
5       5       5       25      125     625     3125    15625
6       6       6       36      216     1296    7776    46656
7       7       7       49      343     2401    16807   117649
8       8       8       64      512     4096    32768   262144
9       9       9       81      729     6561    59049   531441

8
Nhưng bạn sẽ làm gì nếu bạn có 50 cột được thêm như thế này chứ không phải 6?
tối đa

14
@maxtemp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
ostrokach

8
@ostrokach Tôi nghĩ bạn có ý for i, c in enumerate(columns): df[c] = temp[i]. Nhờ vậy, tôi thực sự có được mục đích enumerate: D
rocarvaj

4
Đây là giải pháp thanh lịch và dễ đọc nhất mà tôi từng gặp. Trừ khi bạn gặp vấn đề về hiệu suất, thành ngữ zip(*df['col'].map(function))có lẽ là cách để đi.
François Leblanc


84

Đây là những gì tôi đã làm trong quá khứ

df = pd.DataFrame({'textcol' : np.random.rand(5)})

df
    textcol
0  0.626524
1  0.119967
2  0.803650
3  0.100880
4  0.017859

df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))
   feature1  feature2
0  1.626524 -0.373476
1  1.119967 -0.880033
2  1.803650 -0.196350
3  1.100880 -0.899120
4  1.017859 -0.982141

Chỉnh sửa cho đầy đủ

pd.concat([df, df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))], axis=1)
    textcol feature1  feature2
0  0.626524 1.626524 -0.373476
1  0.119967 1.119967 -0.880033
2  0.803650 1.803650 -0.196350
3  0.100880 1.100880 -0.899120
4  0.017859 1.017859 -0.982141

concat () trông đơn giản hơn merge () để kết nối các cols mới với khung dữ liệu gốc.
thì là

2
câu trả lời hay, bạn không cần sử dụng lệnh chính tả hoặc hợp nhất nếu bạn chỉ định các cột bên ngoài ứng dụngdf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Matt

66

Đây là cách chính xác và dễ nhất để thực hiện điều này cho 95% trường hợp sử dụng:

>>> df = pd.DataFrame(zip(*[range(10)]), columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5

>>> def example(x):
...     x['p1'] = x['num']**2
...     x['p2'] = x['num']**3
...     x['p3'] = x['num']**4
...     return x

>>> df = df.apply(example, axis=1)
>>> df
    num  p1  p2  p3
0    0   0   0    0
1    1   1   1    1
2    2   4   8   16
3    3   9  27   81
4    4  16  64  256

bạn không nên viết: df = df.apply (ví dụ (df), trục = 1) sửa tôi nếu tôi sai, tôi chỉ là người mới
user299791

1
@ user299791, Không trong trường hợp này, bạn đang coi ví dụ là một đối tượng hạng nhất để bạn tự truyền vào hàm. Chức năng này sẽ được áp dụng cho mỗi hàng.
Michael David Watson

chào Michael, câu trả lời của bạn đã giúp tôi trong vấn đề của tôi. Chắc chắn giải pháp của bạn tốt hơn phương thức df.assign () của gấu trúc ban đầu, vì đây là một lần trên mỗi cột. Sử dụng gán (), nếu bạn muốn tạo 2 cột mới, bạn phải sử dụng df1 để làm việc trên df để lấy cột mới 1, sau đó sử dụng df2 để làm việc trên df1 để tạo cột mới thứ hai ... điều này khá đơn điệu. Nhưng phương pháp của bạn đã cứu mạng tôi !!! Cảm ơn!!!
bình luận

1
Sẽ không chạy mã gán cột một lần mỗi hàng? Sẽ không tốt hơn để trả lại một pd.Series({k:v})và tuần tự hóa bài tập cột như trong câu trả lời của Ewan?
Denis de Bernardy 23/07/19

29

Năm 2018, tôi sử dụng apply()với đối sốresult_type='expand'

>>> appiled_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
>>> df = pd.concat([df, appiled_df], axis='columns')

6
Đó là cách bạn làm điều đó, ngày nay!
Make42

1
Điều này đã làm việc ra khỏi hộp vào năm 2020 trong khi nhiều câu hỏi khác thì không. Ngoài ra, nó không sử dụng pd.Series luôn luôn tốt về các vấn đề hiệu suất
Théo Rubenach

1
Đây là một giải pháp tốt. Vấn đề duy nhất là, bạn không thể chọn tên cho 2 cột mới được thêm vào. Sau đó, bạn cần phải làm df.rename (cột = {0: 'col1', 1: 'col2'})
pedram bashiri

2
@pedrambashiri Nếu hàm bạn truyền để df.applytrả về a dict, các cột sẽ xuất hiện được đặt tên theo các phím.
Seb

24

Chỉ dùng result_type="expand"

df = pd.DataFrame(np.random.randint(0,10,(10,2)), columns=["random", "a"])
df[["sq_a","cube_a"]] = df.apply(lambda x: [x.a**2, x.a**3], axis=1, result_type="expand")

4
Nó giúp chỉ ra rằng tùy chọn là mới trong 0,23 . Câu hỏi đã được hỏi lại vào ngày 0.11
smci

Đẹp, điều này là đơn giản và vẫn hoạt động gọn gàng. Đây là một trong những tôi đã tìm kiếm. Cảm ơn
Isaac Sim

Sao chép một câu trả lời trước đó: stackoverflow.com/a/52363890/823470
tar

22

Tóm tắt: Nếu bạn chỉ muốn tạo một vài cột, hãy sử dụngdf[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

Đối với giải pháp này, số lượng cột mới bạn đang tạo phải bằng với số cột bạn sử dụng làm đầu vào cho hàm .apply (). Nếu bạn muốn làm một cái gì đó khác, hãy xem các câu trả lời khác.

Chi tiết Giả sử bạn có khung dữ liệu hai cột. Cột đầu tiên là chiều cao của một người khi họ 10 tuổi; thứ hai là chiều cao của người đó khi họ 20 tuổi.

Giả sử bạn cần tính cả giá trị trung bình của chiều cao của mỗi người và tổng chiều cao của mỗi người. Đó là hai giá trị trên mỗi hàng.

Bạn có thể thực hiện việc này thông qua chức năng sắp được áp dụng sau đây:

def mean_and_sum(x):
    """
    Calculates the mean and sum of two heights.
    Parameters:
    :x -- the values in the row this function is applied to. Could also work on a list or a tuple.
    """

    sum=x[0]+x[1]
    mean=sum/2
    return [mean,sum]

Bạn có thể sử dụng chức năng này như vậy:

 df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

(Để rõ ràng: hàm áp dụng này nhận các giá trị từ mỗi hàng trong khung dữ liệu được đặt lại và trả về một danh sách.)

Tuy nhiên, nếu bạn làm điều này:

df['Mean_&_Sum'] = df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

bạn sẽ tạo 1 cột mới chứa danh sách [trung bình, tổng] mà bạn có thể muốn tránh, vì điều đó sẽ yêu cầu Lambda / Áp dụng khác.

Thay vào đó, bạn muốn chia ra từng giá trị vào cột riêng của nó. Để làm điều này, bạn có thể tạo hai cột cùng một lúc:

df[['Mean','Sum']] = df[['height_at_age_10','height_at_age_20']]
.apply(mean_and_sum(x),axis=1)

4
Đối với gấu trúc 0,23, bạn sẽ cần sử dụng cú pháp:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
SummerEla

Chức năng này có thể gây ra lỗi. Hàm trả về phải là return pd.Series([mean,sum])
K Biếnk Mair

22

Đối với tôi điều này đã làm việc:

Đầu vào df

df = pd.DataFrame({'col x': [1,2,3]})
   col x
0      1
1      2
2      3

Chức năng

def f(x):
    return pd.Series([x*x, x*x*x])

Tạo 2 cột mới:

df[['square x', 'cube x']] = df['col x'].apply(f)

Đầu ra:

   col x  square x  cube x
0      1         1       1
1      2         4       8
2      3         9      27

13

Tôi đã xem xét một số cách để làm điều này và phương pháp được hiển thị ở đây (trả về một loạt gấu trúc) dường như không hiệu quả nhất.

Nếu chúng ta bắt đầu với một khung dữ liệu lớn của dữ liệu ngẫu nhiên:

# Setup a dataframe of random numbers and create a 
df = pd.DataFrame(np.random.randn(10000,3),columns=list('ABC'))
df['D'] = df.apply(lambda r: ':'.join(map(str, (r.A, r.B, r.C))), axis=1)
columns = 'new_a', 'new_b', 'new_c'

Ví dụ hiển thị ở đây:

# Create the dataframe by returning a series
def method_b(v):
    return pd.Series({k: v for k, v in zip(columns, v.split(':'))})
%timeit -n10 -r3 df.D.apply(method_b)

10 vòng, tốt nhất là 3: 2,77 giây trên mỗi vòng lặp

Một phương pháp khác:

# Create a dataframe from a series of tuples
def method_a(v):
    return v.split(':')
%timeit -n10 -r3 pd.DataFrame(df.D.apply(method_a).tolist(), columns=columns)

10 vòng, tốt nhất là 3: 8,85 ms mỗi vòng

Theo tính toán của tôi, việc lấy một loạt các bộ dữ liệu và sau đó chuyển đổi nó thành DataFrame hiệu quả hơn nhiều. Tôi rất muốn nghe suy nghĩ của mọi người mặc dù nếu có lỗi trong công việc của tôi.


Điều này thực sự hữu ích! Tôi đã tăng tốc độ 30 lần so với các phương thức chuỗi trả về hàm.
Pushkar Nimkar

9

Giải pháp được chấp nhận sẽ cực kỳ chậm đối với nhiều dữ liệu. Giải pháp có số lượng upvote lớn nhất là một chút khó đọc và cũng chậm với dữ liệu số. Nếu mỗi cột mới có thể được tính độc lập với các cột khác, tôi sẽ chỉ gán trực tiếp từng cột mà không sử dụng apply.

Ví dụ với dữ liệu ký tự giả

Tạo 100.000 chuỗi trong DataFrame

df = pd.DataFrame(np.random.choice(['he jumped', 'she ran', 'they hiked'],
                                   size=100000, replace=True),
                  columns=['words'])
df.head()
        words
0     she ran
1     she ran
2  they hiked
3  they hiked
4  they hiked

Giả sử chúng tôi muốn trích xuất một số tính năng văn bản như được thực hiện trong câu hỏi ban đầu. Chẳng hạn, hãy trích xuất ký tự đầu tiên, đếm sự xuất hiện của chữ 'e' và viết hoa cụm từ.

df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
df.head()
        words first  count_e         cap
0     she ran     s        1     She ran
1     she ran     s        1     She ran
2  they hiked     t        2  They hiked
3  they hiked     t        2  They hiked
4  they hiked     t        2  They hiked

Thời gian

%%timeit
df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
127 ms ± 585 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

def extract_text_features(x):
    return x[0], x.count('e'), x.capitalize()

%timeit df['first'], df['count_e'], df['cap'] = zip(*df['words'].apply(extract_text_features))
101 ms ± 2.96 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Đáng ngạc nhiên, bạn có thể có hiệu suất tốt hơn bằng cách lặp qua từng giá trị

%%timeit
a,b,c = [], [], []
for s in df['words']:
    a.append(s[0]), b.append(s.count('e')), c.append(s.capitalize())

df['first'] = a
df['count_e'] = b
df['cap'] = c
79.1 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Một ví dụ khác với dữ liệu số giả

Tạo 1 triệu số ngẫu nhiên và kiểm tra powerschức năng từ trên.

df = pd.DataFrame(np.random.rand(1000000), columns=['num'])


def powers(x):
    return x, x**2, x**3, x**4, x**5, x**6

%%timeit
df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
       zip(*df['num'].map(powers))
1.35 s ± 83.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Chỉ định mỗi cột nhanh hơn 25 lần và rất dễ đọc:

%%timeit 
df['p1'] = df['num'] ** 1
df['p2'] = df['num'] ** 2
df['p3'] = df['num'] ** 3
df['p4'] = df['num'] ** 4
df['p5'] = df['num'] ** 5
df['p6'] = df['num'] ** 6
51.6 ms ± 1.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Tôi đã thực hiện một phản hồi tương tự với nhiều chi tiết hơn ở đây về lý do tại sao applythường không phải là cách để đi.


8

Đã đăng cùng một câu trả lời trong hai câu hỏi tương tự khác. Cách tôi thích làm điều này là kết thúc các giá trị trả về của hàm trong một chuỗi:

def f(x):
    return pd.Series([x**2, x**3])

Và sau đó sử dụng áp dụng như sau để tạo các cột riêng biệt:

df[['x**2','x**3']] = df.apply(lambda row: f(row['x']), axis=1)

1

bạn có thể trả về toàn bộ hàng thay vì giá trị:

df = df.apply(extract_text_features,axis = 1)

nơi hàm trả về hàng

def extract_text_features(row):
      row['new_col1'] = value1
      row['new_col2'] = value2
      return row

Không, tôi không muốn áp dụng extract_text_featurescho mọi cột của df, chỉ cho cột văn bảndf.textcol
smci

-2
def myfunc(a):
    return a * a

df['New Column'] = df['oldcolumn'].map(myfunc))

Điều này làm việc cho tôi. Cột mới sẽ được tạo với dữ liệu cột cũ được xử lý.


2
Điều này không trả về 'nhiều cột mới'
pedram bashiri

Điều này không trả về 'nhiều cột mới', vì vậy nó không trả lời câu hỏi. Bạn có thể vui lòng xóa nó?
smci
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.