apply
, Chức năng Tiện lợi mà bạn không bao giờ Cần đến
Chúng tôi bắt đầu bằng cách giải quyết từng câu hỏi trong OP.
" Nếu áp dụng quá tệ, thì tại sao nó lại nằm trong API? "
DataFrame.apply
và Series.apply
là các hàm tiện lợi được định nghĩa trên DataFrame và đối tượng Series tương ứng. apply
chấp nhận bất kỳ chức năng nào do người dùng xác định áp dụng chuyển đổi / tổng hợp trên DataFrame. apply
thực sự là một viên đạn bạc mà bất kỳ chức năng nào hiện có của gấu trúc không thể làm được.
Một số điều apply
có thể làm:
- Chạy bất kỳ chức năng nào do người dùng xác định trên DataFrame hoặc Series
- Áp dụng một hàm theo hàng (
axis=1
) hoặc theo cột ( axis=0
) trên DataFrame
- Thực hiện căn chỉnh chỉ mục trong khi áp dụng chức năng
- Thực hiện tổng hợp với các chức năng do người dùng xác định (tuy nhiên, chúng tôi thường thích
agg
hoặc transform
trong những trường hợp này)
- Thực hiện các phép biến đổi theo phần tử
- Truyền kết quả tổng hợp đến các hàng gốc (xem
result_type
đối số).
- Chấp nhận các đối số vị trí / từ khóa để chuyển đến các hàm do người dùng xác định.
... Trong số những người khác. Để biết thêm thông tin, hãy xem Ứng dụng chức năng theo hàng hoặc theo cột trong tài liệu.
Vì vậy, với tất cả các tính năng, tại sao là apply
xấu? Đó là bởi vì apply
nó là chậm . Pandas không đưa ra giả định nào về bản chất của chức năng của bạn và do đó, áp dụng lặp đi lặp lại chức năng của bạn cho từng hàng / cột nếu cần. Ngoài ra, việc xử lý tất cả các tình huống trên có nghĩa là apply
phải chịu một số chi phí lớn ở mỗi lần lặp. Hơn nữa, apply
tiêu tốn nhiều bộ nhớ hơn, đây là một thách thức đối với các ứng dụng bị giới hạn bộ nhớ.
Có rất ít trường apply
hợp thích hợp để sử dụng (thêm về điều đó bên dưới). Nếu bạn không chắc mình có nên sử dụng hay không apply
, có lẽ bạn không nên.
Hãy giải quyết câu hỏi tiếp theo.
" Làm thế nào và khi nào tôi nên áp dụng mã miễn phí? "
Để diễn đạt lại, đây là một số tình huống phổ biến mà bạn sẽ muốn loại bỏ mọi cuộc gọi tới apply
.
Dữ liệu số
Nếu bạn đang làm việc với dữ liệu số, có thể đã có một chức năng cython được vectơ hóa thực hiện chính xác những gì bạn đang cố gắng thực hiện (nếu không, vui lòng đặt câu hỏi trên Stack Overflow hoặc mở một yêu cầu tính năng trên GitHub).
Đối chiếu hiệu suất của apply
một phép toán cộng đơn giản.
df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df
A B
0 9 12
1 4 7
2 2 5
3 1 4
df.apply(np.sum)
A 16
B 28
dtype: int64
df.sum()
A 16
B 28
dtype: int64
Hiệu suất khôn ngoan, không có sự so sánh, tương đương với số hóa nhanh hơn nhiều. Không cần biểu đồ, vì sự khác biệt là rõ ràng ngay cả đối với dữ liệu đồ chơi.
%timeit df.apply(np.sum)
%timeit df.sum()
2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Ngay cả khi bạn bật truyền mảng thô với raw
đối số, nó vẫn chậm gấp đôi.
%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Một vi dụ khac:
df.apply(lambda x: x.max() - x.min())
A 8
B 8
dtype: int64
df.max() - df.min()
A 8
B 8
dtype: int64
%timeit df.apply(lambda x: x.max() - x.min())
%timeit df.max() - df.min()
2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Nói chung, hãy tìm các giải pháp thay thế được vector hóa nếu có thể.
Chuỗi / Regex
Pandas cung cấp các hàm chuỗi được "vectơ hóa" trong hầu hết các tình huống, nhưng có một số trường hợp hiếm hoi mà các hàm đó không ... "áp dụng", có thể nói như vậy.
Một vấn đề phổ biến là kiểm tra xem một giá trị trong một cột có xuất hiện trong một cột khác của cùng một hàng hay không.
df = pd.DataFrame({
'Name': ['mickey', 'donald', 'minnie'],
'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
'Value': [20, 10, 86]})
df
Name Value Title
0 mickey 20 wonderland
1 donald 10 welcome to donald's castle
2 minnie 86 Minnie mouse clubhouse
Điều này sẽ trả về hàng thứ hai và hàng thứ ba, vì "donald" và "minnie" có trong các cột "Tiêu đề" tương ứng của chúng.
Sử dụng ứng dụng, điều này sẽ được thực hiện bằng cách sử dụng
df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)
0 False
1 True
2 True
dtype: bool
df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
Tuy nhiên, có một giải pháp tốt hơn bằng cách sử dụng khả năng hiểu danh sách.
df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
%timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Điều cần lưu ý ở đây là các quy trình lặp đi lặp lại diễn ra nhanh hơn apply
, vì chi phí thấp hơn. Nếu bạn cần xử lý NaN và các loại dtype không hợp lệ, bạn có thể xây dựng dựa trên điều này bằng cách sử dụng một hàm tùy chỉnh, sau đó bạn có thể gọi với các đối số bên trong khả năng hiểu danh sách.
Để biết thêm thông tin về thời điểm nên coi việc hiểu danh sách là một lựa chọn tốt, hãy xem bài viết của tôi: Đối với vòng lặp với gấu trúc - Khi nào tôi nên quan tâm? .
Lưu ý Các
hoạt động ngày và giờ cũng có các phiên bản được vector hóa. Vì vậy, ví dụ, bạn nên thích pd.to_datetime(df['date'])
, hơn, nói df['date'].apply(pd.to_datetime)
,.
Đọc thêm tại
tài liệu .
Một cạm bẫy chung: Bùng nổ các cột danh sách
s = pd.Series([[1, 2]] * 3)
s
0 [1, 2]
1 [1, 2]
2 [1, 2]
dtype: object
Mọi người bị cám dỗ để sử dụng apply(pd.Series)
. Điều này thật kinh khủng về mặt hiệu suất.
s.apply(pd.Series)
0 1
0 1 2
1 1 2
2 1 2
Một lựa chọn tốt hơn là làm phẳng cột và chuyển nó vào pd.DataFrame.
pd.DataFrame(s.tolist())
0 1
0 1 2
1 1 2
2 1 2
%timeit s.apply(pd.Series)
%timeit pd.DataFrame(s.tolist())
2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Cuối cùng,
" Có tình huống nào apply
tốt không? "
Áp dụng là một chức năng tiện lợi, vì vậy có những tình huống mà chi phí không đáng kể đủ để tha thứ. Nó thực sự phụ thuộc vào số lần hàm được gọi.
Các hàm được Vectơ hóa cho Chuỗi chứ không phải DataFrames
Nếu bạn muốn áp dụng một thao tác chuỗi trên nhiều cột thì sao? Điều gì xảy ra nếu bạn muốn chuyển đổi nhiều cột thành datetime? Các hàm này chỉ được biểu diễn hóa cho Sê-ri, vì vậy chúng phải được áp dụng trên mỗi cột mà bạn muốn chuyển đổi / hoạt động.
df = pd.DataFrame(
pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2),
columns=['date1', 'date2'])
df
date1 date2
0 2018-12-31 2019-01-02
1 2019-01-04 2019-01-06
2 2019-01-08 2019-01-10
3 2019-01-12 2019-01-14
4 2019-01-16 2019-01-18
5 2019-01-20 2019-01-22
6 2019-01-24 2019-01-26
7 2019-01-28 2019-01-30
df.dtypes
date1 object
date2 object
dtype: object
Đây là một trường hợp được chấp nhận cho apply
:
df.apply(pd.to_datetime, errors='coerce').dtypes
date1 datetime64[ns]
date2 datetime64[ns]
dtype: object
Lưu ý rằng nó cũng có ý nghĩa stack
hoặc chỉ sử dụng một vòng lặp rõ ràng. Tất cả các tùy chọn này nhanh hơn một chút so với sử dụng apply
, nhưng sự khác biệt đủ nhỏ để tha thứ.
%timeit df.apply(pd.to_datetime, errors='coerce')
%timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
%timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')
5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Bạn có thể tạo một trường hợp tương tự cho các hoạt động khác như hoạt động chuỗi hoặc chuyển đổi thành danh mục.
u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))
v / s
u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
v = df.copy()
for c in df:
v[c] = df[c].astype(category)
Và như thế...
Chuyển đổi chuỗi thành str
: astype
so vớiapply
Điều này có vẻ giống như một đặc điểm riêng của API. Việc sử dụng apply
để chuyển đổi số nguyên trong Chuỗi thành chuỗi có thể so sánh được (và đôi khi nhanh hơn) so với việc sử dụng astype
.
Biểu đồ được vẽ bằng cách sử dụng perfplot
thư viện.
import perfplot
perfplot.show(
setup=lambda n: pd.Series(np.random.randint(0, n, n)),
kernels=[
lambda s: s.astype(str),
lambda s: s.apply(str)
],
labels=['astype', 'apply'],
n_range=[2**k for k in range(1, 20)],
xlabel='N',
logx=True,
logy=True,
equality_check=lambda x, y: (x == y).all())
Với phao, tôi thấy astype
nó luôn nhanh bằng hoặc nhanh hơn một chút apply
. Vì vậy, điều này liên quan đến thực tế là dữ liệu trong thử nghiệm là kiểu số nguyên.
GroupBy
hoạt động với các phép biến đổi chuỗi
GroupBy.apply
vẫn chưa được thảo luận cho đến bây giờ, nhưng GroupBy.apply
cũng là một hàm tiện lợi lặp đi lặp lại để xử lý bất cứ thứ gì mà các GroupBy
hàm hiện có không có.
Một yêu cầu phổ biến là thực hiện một GroupBy và sau đó là hai phép toán nguyên tố, chẳng hạn như "lagged cumsum":
df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
df
A B
0 a 12
1 a 7
2 b 5
3 c 4
4 c 5
5 c 4
6 d 3
7 d 2
8 e 1
9 e 10
Bạn sẽ cần hai cuộc gọi theo nhóm liên tiếp ở đây:
df.groupby('A').B.cumsum().groupby(df.A).shift()
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
Sử dụng apply
, bạn có thể rút ngắn cuộc gọi này thành một cuộc gọi duy nhất.
df.groupby('A').B.apply(lambda x: x.cumsum().shift())
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
Rất khó để định lượng hiệu suất vì nó phụ thuộc vào dữ liệu. Nhưng nói chung, apply
là một giải pháp có thể chấp nhận được nếu mục đích là giảm một groupby
cuộc gọi (vì groupby
cũng khá tốn kém).
Những lưu ý khác
Bên cạnh những lưu ý đã đề cập ở trên, cũng cần nhắc lại rằng apply
hoạt động trên hàng (hoặc cột) đầu tiên hai lần. Điều này được thực hiện để xác định xem chức năng có bất kỳ tác dụng phụ nào không. Nếu không, apply
có thể sử dụng đường dẫn nhanh để đánh giá kết quả, nếu không, nó sẽ rơi vào tình trạng triển khai chậm.
df = pd.DataFrame({
'A': [1, 2],
'B': ['x', 'y']
})
def func(x):
print(x['A'])
return x
df.apply(func, axis=1)
# 1
# 1
# 2
A B
0 1 x
1 2 y
Hành vi này cũng được thấy GroupBy.apply
trên các phiên bản gấu trúc <0,25 (nó đã được sửa cho 0,25, xem tại đây để biết thêm thông tin .)
returns.add(1).apply(np.log)
so vớinp.log(returns.add(1)
là một trường hợpapply
thường sẽ nhanh hơn một chút, đó là hộp màu xanh lá cây phía dưới bên phải trong biểu đồ của jpp bên dưới.