Hiệu suất của Pandas áp dụng so với np.vectorize để tạo cột mới từ các cột hiện có


81

Tôi đang sử dụng khung dữ liệu Pandas và muốn tạo một cột mới làm chức năng của các cột hiện có. Tôi đã không thấy một cuộc thảo luận tốt về sự khác biệt tốc độ giữa df.apply()np.vectorize(), vì vậy tôi nghĩ tôi sẽ hỏi ở đây.

apply()Chức năng Pandas chậm. Từ những gì tôi đo được (hiển thị bên dưới trong một số thử nghiệm), việc sử dụng np.vectorize()nhanh hơn 25 lần (hoặc hơn) so với sử dụng chức năng DataFrame apply(), ít nhất là trên MacBook Pro 2016 của tôi. Đây có phải là một kết quả mong đợi, và tại sao?

Ví dụ: giả sử tôi có khung dữ liệu sau với Ncác hàng:

N = 10
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
df.head()
#     A   B
# 0  78  50
# 1  23  91
# 2  55  62
# 3  82  64
# 4  99  80

Giả sử thêm rằng tôi muốn tạo một cột mới dưới dạng một hàm của hai cột AB. Trong ví dụ dưới đây, tôi sẽ sử dụng một hàm đơn giản divide(). Để áp dụng chức năng, tôi có thể sử dụng df.apply()hoặc np.vectorize():

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)

df['result2'] = np.vectorize(divide)(df['A'], df['B'])

df.head()
#     A   B    result   result2
# 0  78  50  1.560000  1.560000
# 1  23  91  0.252747  0.252747
# 2  55  62  0.887097  0.887097
# 3  82  64  1.281250  1.281250
# 4  99  80  1.237500  1.237500

Nếu tôi tăng lên Nkích thước trong thế giới thực như 1 triệu hoặc hơn, thì tôi quan sát thấy con số đó np.vectorize()nhanh hơn 25 lần hoặc hơn df.apply().

Dưới đây là một số mã điểm chuẩn hoàn chỉnh:

import pandas as pd
import numpy as np
import time

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

for N in [1000, 10000, 100000, 1000000, 10000000]:    

    print ''
    A_list = np.random.randint(1, 100, N)
    B_list = np.random.randint(1, 100, N)
    df = pd.DataFrame({'A': A_list, 'B': B_list})

    start_epoch_sec = int(time.time())
    df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
    end_epoch_sec = int(time.time())
    result_apply = end_epoch_sec - start_epoch_sec

    start_epoch_sec = int(time.time())
    df['result2'] = np.vectorize(divide)(df['A'], df['B'])
    end_epoch_sec = int(time.time())
    result_vectorize = end_epoch_sec - start_epoch_sec


    print 'N=%d, df.apply: %d sec, np.vectorize: %d sec' % \
            (N, result_apply, result_vectorize)

    # Make sure results from df.apply and np.vectorize match.
    assert(df['result'].equals(df['result2']))

Các kết quả được hiển thị dưới đây:

N=1000, df.apply: 0 sec, np.vectorize: 0 sec

N=10000, df.apply: 1 sec, np.vectorize: 0 sec

N=100000, df.apply: 2 sec, np.vectorize: 0 sec

N=1000000, df.apply: 24 sec, np.vectorize: 1 sec

N=10000000, df.apply: 262 sec, np.vectorize: 4 sec

Nếu np.vectorize()nói chung luôn luôn nhanh hơn df.apply(), thì tại sao np.vectorize()không được đề cập nhiều hơn? Tôi chỉ thấy các bài đăng trên StackOverflow liên quan đến df.apply(), chẳng hạn như:

gấu trúc tạo cột mới dựa trên các giá trị từ các cột khác

Làm cách nào để sử dụng chức năng 'áp dụng' của Pandas cho nhiều cột?

Cách áp dụng một hàm cho hai cột của khung dữ liệu Pandas


Tôi didnt đào vào các chi tiết của bạn đặt câu hỏi nhưng np.vectorizevề cơ bản là một con trăn forvòng lặp (đó là một phương pháp tiện lợi) và applyvới một lambda cũng là trong thời gian trăn
roganjosh

"Nếu np.vectorize () nói chung luôn nhanh hơn df.apply (), thì tại sao np.vectorize () không được đề cập nhiều hơn?" Bởi vì bạn không nên sử dụng applytheo từng hàng trừ khi bạn phải làm như vậy, và rõ ràng là một hàm được vectơ hóa sẽ thực hiện tốt hơn hàm không được vectơ hóa.
PMende

1
@PMende nhưng np.vectorizekhông được vector hóa. Đó là một cái tên nhầm lẫn nổi tiếng
roganjosh

1
@PMende, Chắc chắn rồi, tôi không ngụ ý khác. Bạn không nên lấy ý kiến ​​của mình về việc triển khai từ thời gian. Vâng, họ rất sâu sắc. Nhưng chúng có thể khiến bạn đoán những điều không đúng.
jpp

3
@PMende chơi đùa với những người truy cập gấu trúc .str. Chúng chậm hơn khả năng hiểu danh sách trong nhiều trường hợp. Chúng tôi giả định quá nhiều.
roganjosh

Câu trả lời:


115

Tôi sẽ bắt đầu bằng cách nói rằng sức mạnh của mảng Pandas và NumPy có được từ các phép tính vectorised hiệu suất cao trên mảng số. 1 Toàn bộ điểm của các phép tính vecto là tránh các vòng lặp cấp Python bằng cách chuyển các phép tính sang mã C được tối ưu hóa cao và sử dụng các khối bộ nhớ liền kề. 2

Vòng lặp cấp Python

Bây giờ chúng ta có thể xem xét một số thời gian. Dưới đây là tất cả các vòng Python cấp sản xuất hoặc pd.Series, np.ndarrayhoặc listđối tượng chứa các giá trị như nhau. Với mục đích gán cho một chuỗi trong khung dữ liệu, các kết quả có thể so sánh được.

# Python 3.6.5, NumPy 1.14.3, Pandas 0.23.0

np.random.seed(0)
N = 10**5

%timeit list(map(divide, df['A'], df['B']))                                   # 43.9 ms
%timeit np.vectorize(divide)(df['A'], df['B'])                                # 48.1 ms
%timeit [divide(a, b) for a, b in zip(df['A'], df['B'])]                      # 49.4 ms
%timeit [divide(a, b) for a, b in df[['A', 'B']].itertuples(index=False)]     # 112 ms
%timeit df.apply(lambda row: divide(*row), axis=1, raw=True)                  # 760 ms
%timeit df.apply(lambda row: divide(row['A'], row['B']), axis=1)              # 4.83 s
%timeit [divide(row['A'], row['B']) for _, row in df[['A', 'B']].iterrows()]  # 11.6 s

Một số điểm cần lưu ý:

  1. Các tuplephương pháp dựa trên (4 đầu tiên) là một yếu tố hiệu quả hơn pd.Seriescác phương pháp dựa trên (3 cuối cùng).
  2. np.vectorize, liệt kê hiểu + zipmapcác phương pháp, tức là 3 phương pháp hàng đầu, đều có hiệu suất gần như giống nhau. Điều này là do họ sử dụng tuple bỏ qua một số chi phí Pandas từ pd.DataFrame.itertuples.
  3. Có một sự cải thiện tốc độ đáng kể từ việc sử dụng raw=Truevới pd.DataFrame.applyso với không. Tùy chọn này cấp các mảng NumPy cho hàm tùy chỉnh thay vì các pd.Seriesđối tượng.

pd.DataFrame.apply: chỉ là một vòng lặp khác

Để xem chính xác các đối tượng mà Pandas đi qua, bạn có thể sửa đổi chức năng của mình một cách đáng kể:

def foo(row):
    print(type(row))
    assert False  # because you only need to see this once
df.apply(lambda row: foo(row), axis=1)

Output: <class 'pandas.core.series.Series'>. Tạo, chuyển và truy vấn một đối tượng chuỗi Pandas mang các chi phí quan trọng liên quan đến mảng NumPy. Điều này không có gì đáng ngạc nhiên: Chuỗi gấu trúc bao gồm một lượng lớn giàn giáo để chứa chỉ mục, giá trị, thuộc tính, v.v.

Thực hiện lại bài tập tương tự raw=Truevà bạn sẽ thấy <class 'numpy.ndarray'>. Tất cả điều này được mô tả trong tài liệu, nhưng nhìn thấy nó thì thuyết phục hơn.

np.vectorize: vectorisation giả

Các tài liệu cho np.vectorizecó ghi chú sau:

Hàm vectơ đánh giá pyfunctrên các bộ giá trị liên tiếp của mảng đầu vào giống như hàm bản đồ python, ngoại trừ hàm này sử dụng các quy tắc phát sóng của numpy.

"Quy tắc phát sóng" không liên quan ở đây, vì các mảng đầu vào có cùng kích thước. Song song với maplà hướng dẫn, vì mapphiên bản trên có hiệu suất gần như giống hệt nhau. Các mã nguồn chương trình gì đang xảy ra: np.vectorizechuyển đổi chức năng đầu vào của bạn thành một chức năng phổ quát ( "ufunc") thông qua np.frompyfunc. Có một số tối ưu hóa, ví dụ như bộ nhớ đệm, có thể dẫn đến một số cải thiện hiệu suất.

Nói tóm lại, np.vectorizevòng lặp cấp Python sẽ làm những gì, nhưng pd.DataFrame.applythêm một chi phí lớn. Không có biên dịch JIT nào mà bạn thấy numba(xem bên dưới). Nó chỉ là một sự tiện lợi .

Vectorisation đích thực: những gì bạn nên sử dụng

Tại sao sự khác biệt trên không được đề cập ở bất kỳ đâu? Bởi vì hiệu suất của các phép tính được vecto thực sự làm cho chúng không liên quan:

%timeit np.where(df['B'] == 0, 0, df['A'] / df['B'])       # 1.17 ms
%timeit (df['A'] / df['B']).replace([np.inf, -np.inf], 0)  # 1.96 ms

Vâng, nhanh hơn ~ 40 lần so với tốc độ nhanh nhất trong số các giải pháp lặp lại ở trên. Một trong hai điều này đều được chấp nhận. Theo tôi, đầu tiên là cô đọng, dễ đọc và hiệu quả. Chỉ xem xét các phương pháp khác, ví dụ như numbabên dưới, nếu hiệu suất là quan trọng và đây là một phần của nút thắt cổ chai của bạn.

numba.njit: hiệu quả cao hơn

Khi các vòng lặp được coi là khả thi, chúng thường được tối ưu hóa thông qua numbacác mảng NumPy bên dưới để di chuyển đến C. nhiều nhất có thể.

Thật vậy, numbacải thiện hiệu suất đến micro giây . Nếu không có một số công việc rườm rà, sẽ khó đạt được hiệu quả cao hơn thế này.

from numba import njit

@njit
def divide(a, b):
    res = np.empty(a.shape)
    for i in range(len(a)):
        if b[i] != 0:
            res[i] = a[i] / b[i]
        else:
            res[i] = 0
    return res

%timeit divide(df['A'].values, df['B'].values)  # 717 µs

Việc sử dụng @njit(parallel=True)có thể cung cấp một sự thúc đẩy hơn nữa cho các mảng lớn hơn.


1 loại Numeric bao gồm: int, float, datetime, bool, category. Chúng loại trừ object dtype và có thể được giữ trong các khối bộ nhớ liền kề.

2 Có ít nhất 2 lý do tại sao các hoạt động NumPy lại hiệu quả so với Python:

  • Mọi thứ trong Python đều là một đối tượng. Điều này bao gồm, không giống như C, các số. Do đó, các loại Python có một chi phí không tồn tại với các loại C gốc.
  • Các phương thức NumPy thường dựa trên C. Ngoài ra, các thuật toán tối ưu hóa được sử dụng nếu có thể.

1
@jpp: Sử dụng trình trang trí với parallelđối số @njit(parallel=True)mang lại cho tôi một cải tiến hơn nữa so với chỉ @njit. Có lẽ bạn cũng có thể thêm điều đó.
Sheldore

1
Bạn có một kiểm tra kỹ cho b [i]! = 0. Hành vi thông thường của Python và Numba là kiểm tra 0 và thông báo lỗi. Điều này có thể phá vỡ bất kỳ vector hóa SIMD nào và thường có ảnh hưởng lớn đến tốc độ thực thi. Nhưng bạn có thể thay đổi điều đó trong Numba thành @njit (error_model = 'numpy') để tránh việc kiểm tra hai lần này cho phép chia cho 0. Bạn cũng nên cấp phát bộ nhớ với np.empty và đặt kết quả thành 0 trong một câu lệnh else.
max9111

1
error_model numpy sử dụng những gì bộ xử lý cung cấp trong phép chia cho 0 -> NaN. Ít nhất trong Numba 0.41dev cả hai phiên bản đều sử dụng phương pháp vector hóa SIMD. Bạn có thể kiểm tra điều này như được mô tả tại đây numba.pydata.org/numba-doc/dev/user/faq.html (1.16.2.3. Tại sao vòng lặp của tôi không được vector hóa?) Tôi chỉ cần thêm một câu lệnh khác vào hàm của bạn (res [ i] = 0.) và tập trung bộ nhớ bằng np.empty. Điều này sẽ kết hợp với error_model = 'numpy' cải thiện hiệu suất khoảng 20%. Trên các phiên bản Numba cũ đã có một ảnh hưởng cao hơn về hiệu suất ...
max9111

2
@ stackoverflowuser2010, Không có câu trả lời chung "cho các chức năng tùy ý". Bạn phải chọn đúng công cụ cho công việc phù hợp, đó là một phần của việc hiểu lập trình / thuật toán.
jpp

1
Những ngày nghỉ vui vẻ!
cs95

5

Các chức năng của bạn càng phức tạp (tức là, càng ít numpycó thể di chuyển đến bên trong của chính nó), bạn sẽ càng thấy rằng hiệu suất sẽ không khác biệt như vậy. Ví dụ:

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=100000))

def parse_name(name):
    if name.lower().startswith('a'):
        return 'A'
    elif name.lower().startswith('e'):
        return 'E'
    elif name.lower().startswith('i'):
        return 'I'
    elif name.lower().startswith('o'):
        return 'O'
    elif name.lower().startswith('u'):
        return 'U'
    return name

parse_name_vec = np.vectorize(parse_name)

Thực hiện một số thời gian:

Sử dụng Áp dụng

%timeit name_series.apply(parse_name)

Các kết quả:

76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Sử dụng np.vectorize

%timeit parse_name_vec(name_series)

Các kết quả:

77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Numpy cố gắng biến các hàm của python thành ufunccác đối tượng numpy khi bạn gọi np.vectorize. Làm thế nào nó thực hiện điều này, tôi thực sự không biết - bạn sẽ phải tìm hiểu sâu hơn về bên trong của numpy hơn là tôi sẵn sàng ATM. Điều đó nói rằng, nó dường như hoạt động tốt hơn trên các hàm số đơn giản hơn là hàm dựa trên chuỗi này ở đây.

Crank kích thước lên đến 1.000.000:

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))

apply

%timeit name_series.apply(parse_name)

Các kết quả:

769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

np.vectorize

%timeit parse_name_vec(name_series)

Các kết quả:

794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Một cách tốt hơn (được vector hóa ) với np.select:

cases = [
    name_series.str.lower().str.startswith('a'), name_series.str.lower().str.startswith('e'),
    name_series.str.lower().str.startswith('i'), name_series.str.lower().str.startswith('o'),
    name_series.str.lower().str.startswith('u')
]
replacements = 'A E I O U'.split()

Thời gian:

%timeit np.select(cases, replacements, default=name_series)

Các kết quả:

67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Điều gì sẽ xảy ra nếu bạn quay số đó lên đến size=1000000(1 triệu)?
stackoverflowuser2010

2
Tôi khá chắc chắn rằng khẳng định của bạn ở đây là không chính xác. Tôi không thể trở lại rằng tuyên bố với mã cho bây giờ, hy vọng người khác có thể
roganjosh

@ stackoverflowuser2010 Tôi đã cập nhật nó, cùng với một phương pháp vector hóa thực tế .
PMende

0

Tôi mới làm quen với python. Nhưng trong ví dụ dưới đây, 'apply' dường như hoạt động nhanh hơn 'vectorize', hoặc tôi đang thiếu thứ gì đó.

 import numpy as np
 import pandas as pd

 B = np.random.rand(1000,1000)
 fn = np.vectorize(lambda l: 1/(1-np.exp(-l)))
 print(fn(B))

 B = pd.DataFrame(np.random.rand(1000,1000))
 fn = lambda l: 1/(1-np.exp(-l))
 print(B.apply(fn))
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.