Nhiều người dùng .apply() để “tránh vòng lặp” và kỳ vọng nó vừa nhanh vừa đơn giản. Thực tế, .apply(axis=1) theo từng hàng vẫn là vòng lặp ở mức Python. Nó có thể chậm trên dữ liệu lớn, và đôi khi trả về hình dạng bạn không mong đợi. Cách khắc phục rất đơn giản: dùng các phép toán vector hóa của pandas/NumPy cho các tác vụ phổ biến, và chỉ dùng .apply() cho logic thực sự cần nhiều cột.
Hướng dẫn này cho thấy cách .apply() hoạt động hiện nay, nêu bật các cạm bẫy thường gặp, và cung cấp các mẫu thay thế nhanh hơn, rõ ràng hơn mà bạn có thể áp dụng ngay.
DataFrame.apply() và Series.apply() là gì?
DataFrame.apply(func, axis=0) mặc định gọi func trên từng cột. Với axis=1, nó gọi func trên từng hàng. Series.apply(func) gọi func trên từng phần tử (hoặc toàn bộ Series, tùy theo chữ ký hàm ở các phiên bản gần đây).
Theo hàng so với theo cột:axis=1 nghĩa là “mỗi hàng”. axis=0 (mặc định) nghĩa là “mỗi cột”.
Quy tắc hình dạng trả về (pandas ≥0.23): Nếu hàm của bạn trả về một vô hướng cho mỗi hàng/cột, bạn nhận được một Series. Nếu nó trả về một Series hoặc dict, pandas sẽ mở rộng các khóa đó thành một DataFrame. Nếu nó trả về một list/mảng, bạn sẽ nhận một Series các list trừ khi bạn đặt result_type='expand'.
Lỗi nghiêm ngặt hơn (pandas ≥2.0): Các độ dài dạng list không khớp giờ sẽ gây lỗi thay vì âm thầm tạo kết quả không nhất quán.
Lựa chọn ưu tiên: Các phép toán vector hóa
Hầu hết các tác vụ kết hợp hoặc biến đổi cột sẽ nhanh và rõ ràng hơn với biểu thức vector hóa hoặc phương thức dựng sẵn. Ví dụ sau tính hiệu số điểm (run differential) của một đội bóng chày mà không cần .apply().
import pandas as pd
team_stats = pd.DataFrame({"Team":["ARI","ATL","BAL"],"League":["NL","NL","AL"],"Year":[2012,2012,2012],"RunsScored":[734,700,712],"RunsAllowed":[688,600,705],"Wins":[81,94,93],"Games":[162,162,162],"Playoffs":[0,1,1],})# Vectorized: fast and idiomatic
team_stats["RunDiff"]= team_stats["RunsScored"]- team_stats["RunsAllowed"]print(team_stats[["Team","RunsScored","RunsAllowed","RunDiff"]])Được hỗ trợ bởi
.apply() theo hàng khi bạn thực sự cần
Dùng .apply(axis=1) khi logic của bạn thực sự trải qua nhiều cột và khó vector hóa (ví dụ, các quy tắc điều kiện phụ thuộc vào nhiều trường).
tính một giá trị dẫn xuất từ nhiều cột
Mẫu này tính một giá trị cho mỗi hàng bằng nhiều đầu vào. Cách tiếp cận vector hóa ở trên vẫn được ưu tiên, nhưng phần này cho thấy cách dùng đúng theo hàng và các tùy chọn ảnh hưởng đến tốc độ và đầu ra.
defcompute_run_diff(row):# Treat the row as read-only; return a scalarreturn row["RunsScored"]- row["RunsAllowed"]# Row-wise apply (Python-level loop; can be slow on large data)
team_stats["RunDiff_apply"]= team_stats.apply(compute_run_diff, axis=1)Được hỗ trợ bởi
Khi hàm là số học và chỉ cần mảng thô, truyền raw=True để bỏ qua một số chi phí của pandas bằng cách cung cấp một mảng NumPy cho hàm của bạn.
import numpy as np
defcompute_run_diff_raw(values):# values is a NumPy array when raw=True# Order matches the column order we select
rs, ra = values
return rs - ra
team_stats["RunDiff_raw"]= team_stats[["RunsScored","RunsAllowed"]].apply(
compute_run_diff_raw, axis=1, raw=True)Được hỗ trợ bởi
xác minh đầu vào của hàm một lần
Khi tôi đấu nối một hàm theo hàng mới, tôi in ra một hàng duy nhất để xác nhận dữ liệu được truyền vào.
defdebug_row(row):# Print the first row onlyif row.name == team_stats.index[0]:print("Example row:", row.to_dict())return0
_ = team_stats.apply(debug_row, axis=1)Được hỗ trợ bởi
Nghiên cứu tình huống: Tổng, tổng mùa giải và cờ văn bản
Với thống kê Rays theo năm, hãy ưu tiên các phương thức dựng sẵn và công cụ theo phần tử thay cho .apply() khi có thể.
rays_by_year = pd.DataFrame({"Year":[2012,2011,2010,2009,2008],"RunsScored":[697,707,802,803,774],"RunsAllowed":[577,614,649,754,671],"Wins":[90,91,96,84,97],"Playoffs":[0,1,1,0,1],}).set_index("Year")Được hỗ trợ bởi
lấy tổng theo cột một cách hiệu quả
Dùng DataFrame.sum() thay vì .apply(sum). Cách này nhanh và rõ ràng hơn.
# Preferred
totals = rays_by_year.sum(axis=0)print(totals)# If you must use apply (not recommended here)
totals_apply = rays_by_year.apply(sum, axis=0)Được hỗ trợ bởi
tính tổng số lần ghi điểm trong một mùa
Dùng số học vector hóa giữa các cột; không cần .apply().
rays_by_year["TotalRuns"]= rays_by_year["RunsScored"]+ rays_by_year["RunsAllowed"]print(rays_by_year[["RunsScored","RunsAllowed","TotalRuns"]].head())Được hỗ trợ bởi
chuyển cờ 0/1 thành văn bản
Ưu tiên Series.map() hoặc replace() cho các phép biến đổi theo phần tử trên một cột.
# Using map with a dict
rays_by_year["PlayoffsText"]= rays_by_year["Playoffs"].map({0:"No",1:"Yes"})# Equivalent with replace
rays_by_year["PlayoffsText2"]= rays_by_year["Playoffs"].replace({0:"No",1:"Yes"})Được hỗ trợ bởi
Kiểm soát hình dạng đầu ra từ .apply()
Khi hàm của bạn trả về nhiều hơn một giá trị cho mỗi hàng, hãy làm rõ hình dạng. Điều này tránh bất ngờ và hỏng vỡ ở các phiên bản tương lai.
trả về một list và mở rộng thành các cột
Dùng result_type='expand' để tách một list/mảng thành nhiều cột.
defwins_losses(row):# Derive wins and losses as two outputs
wins = row["Wins"]
losses = row["Games"]- row["Wins"]if"Games"in row else np.nan
return[wins, losses]# Example using team_stats, which has "Wins" and "Games"
expanded = team_stats.apply(wins_losses, axis=1, result_type="expand")
expanded.columns =["WinsOut","LossesOut"]
team_stats = pd.concat([team_stats, expanded], axis=1)Được hỗ trợ bởi
trả về một series hoặc dict để tự động đặt tên cột
Trả về một Series hoặc dict từ mỗi hàng sẽ tạo ra một DataFrame với nhãn cột tương ứng.
.apply(axis=1) theo hàng rất tiện nhưng chậm trên các khung dữ liệu lớn vì nó gọi hàm Python của bạn một lần cho mỗi hàng. Các mẫu dưới đây giúp tránh nút thắt này.
Ưu tiên các phép toán vector hóa của pandas/NumPy và các phương thức dựng sẵn như sum, mean, clip, where, astype, và các phương thức xử lý chuỗi/bộ truy cập.
Với các phép toán số học theo hàng/cột chấp nhận mảng, hãy truyền raw=True để nhận mảng NumPy và giảm chi phí của pandas.
Với công việc theo phần tử trên một cột, dùng Series.map() hoặc các phương thức vector hóa thay cho DataFrame.apply(axis=1).
Tránh thay đổi hàng/cột đầu vào bên trong hàm; thay vào đó hãy trả về một giá trị mới.
Ghi chú phiên bản bạn nên biết (pandas 2.x)
Các bản phát hành pandas gần đây đã siết chặt hành vi quanh .apply() để kết quả dễ dự đoán hơn.
Giá trị trả về dạng list: Từ 2.0+, các độ dài list không khớp sẽ gây lỗi. Hãy dùng độ dài nhất quán và đặt result_type='expand' khi bạn muốn nhiều cột.
Mở rộng đầu ra: Trả về một Series hoặc dict sẽ được mở rộng thành một DataFrame với các khóa đó làm cột (ổn định từ 0.23).
Series.apply() thay đổi: Tham số convert_dtype đã bị loại bỏ. Nếu bạn cần kiểu hỗn hợp, hãy ép sang object trước (ví dụ, s.astype("object").apply(fn)). Các chữ ký mới cho phép kiểm soát việc fn nhận vô hướng hay một Series trong nhiều ngữ cảnh hơn (ví dụ,by_row).
.apply() so với .map() so với .applymap() so với .agg()
Hãy chọn API phù hợp với hình dạng của phép biến đổi bạn cần.
Series.map(func_or_dict): Biến đổi theo phần tử trên một cột. Tốt nhất cho tra cứu hoặc hàm đơn giản.
DataFrame.apply(func, axis=1): Logic theo hàng cần nhiều cột.
DataFrame.apply(func, axis=0): Logic theo cột (mỗi đầu vào là một Series đại diện cho một cột).
DataFrame.applymap(func): Theo phần tử trên mọi ô. Dùng hạn chế; phương thức vector hóa nhanh hơn.
.agg()/.transform(): Tổng hợp và biến đổi theo nhóm; ưu tiên dùng trong các pipeline groupby.
Lỗi thường gặp và cách khắc phục nhanh
Đây là những vấn đề thường khiến kết quả sai hoặc chậm, kèm cách sửa.
Quên axis=1 cho logic theo hàng: Nếu hàm của bạn đột nhiên nhận cột thay vì hàng, hãy thêm axis=1.
Series chứa list ngoài ý muốn: Khi trả về một list/mảng từ mỗi hàng, hãy đặt result_type='expand' để tách thành các cột.
Mã theo hàng chậm: Thay bằng biểu thức vector hóa hoặc phương thức dựng sẵn; nếu không thể, cân nhắc raw=True để giảm chi phí.
Thay đổi hàng đầu vào: Xem đầu vào là chỉ đọc và trả về giá trị mới.
Trôi kiểu dữ liệu (dtype drift): Nếu hàm của bạn đôi khi trả về không phải số nguyên, các cột số nguyên có thể bị nâng cấp lên float hoặc object. Hãy ép kiểu lại bằng astype nếu cần.
Kết luận
.apply() là một công cụ linh hoạt, nhưng không phải lối tắt về hiệu năng. Hãy dùng các phép toán vector hóa cho số học, tổng hợp, và biến đổi theo phần tử trên một cột. Chỉ dùng DataFrame.apply(axis=1) khi logic của bạn thực sự cần nhiều cột trên mỗi hàng. Khi dùng, hãy kiểm soát hình dạng đầu ra bằng result_type, cân nhắc raw=True cho các hàm số, và chú ý đến dtypes. Những mẫu này cho kết quả có thể dự đoán trên pandas hiện đại và mở rộng tốt khi dữ liệu của bạn tăng lên.
Nhiều người dùng
.apply()để “tránh vòng lặp” và kỳ vọng nó vừa nhanh vừa đơn giản. Thực tế,.apply(axis=1)theo từng hàng vẫn là vòng lặp ở mức Python. Nó có thể chậm trên dữ liệu lớn, và đôi khi trả về hình dạng bạn không mong đợi. Cách khắc phục rất đơn giản: dùng các phép toán vector hóa của pandas/NumPy cho các tác vụ phổ biến, và chỉ dùng.apply()cho logic thực sự cần nhiều cột.Hướng dẫn này cho thấy cách
.apply()hoạt động hiện nay, nêu bật các cạm bẫy thường gặp, và cung cấp các mẫu thay thế nhanh hơn, rõ ràng hơn mà bạn có thể áp dụng ngay.DataFrame.apply() và Series.apply() là gì?
DataFrame.apply(func, axis=0)mặc định gọifunctrên từng cột. Vớiaxis=1, nó gọifunctrên từng hàng.Series.apply(func)gọifunctrên từng phần tử (hoặc toàn bộSeries, tùy theo chữ ký hàm ở các phiên bản gần đây).axis=1nghĩa là “mỗi hàng”.axis=0(mặc định) nghĩa là “mỗi cột”.Series. Nếu nó trả về mộtSerieshoặcdict, pandas sẽ mở rộng các khóa đó thành mộtDataFrame. Nếu nó trả về một list/mảng, bạn sẽ nhận một Series các list trừ khi bạn đặtresult_type='expand'.Lựa chọn ưu tiên: Các phép toán vector hóa
Hầu hết các tác vụ kết hợp hoặc biến đổi cột sẽ nhanh và rõ ràng hơn với biểu thức vector hóa hoặc phương thức dựng sẵn. Ví dụ sau tính hiệu số điểm (run differential) của một đội bóng chày mà không cần
.apply()..apply() theo hàng khi bạn thực sự cần
Dùng
.apply(axis=1)khi logic của bạn thực sự trải qua nhiều cột và khó vector hóa (ví dụ, các quy tắc điều kiện phụ thuộc vào nhiều trường).tính một giá trị dẫn xuất từ nhiều cột
Mẫu này tính một giá trị cho mỗi hàng bằng nhiều đầu vào. Cách tiếp cận vector hóa ở trên vẫn được ưu tiên, nhưng phần này cho thấy cách dùng đúng theo hàng và các tùy chọn ảnh hưởng đến tốc độ và đầu ra.
Khi hàm là số học và chỉ cần mảng thô, truyền
raw=Trueđể bỏ qua một số chi phí của pandas bằng cách cung cấp một mảng NumPy cho hàm của bạn.xác minh đầu vào của hàm một lần
Khi tôi đấu nối một hàm theo hàng mới, tôi in ra một hàng duy nhất để xác nhận dữ liệu được truyền vào.
Nghiên cứu tình huống: Tổng, tổng mùa giải và cờ văn bản
Với thống kê Rays theo năm, hãy ưu tiên các phương thức dựng sẵn và công cụ theo phần tử thay cho
.apply()khi có thể.lấy tổng theo cột một cách hiệu quả
Dùng
DataFrame.sum()thay vì.apply(sum). Cách này nhanh và rõ ràng hơn.tính tổng số lần ghi điểm trong một mùa
Dùng số học vector hóa giữa các cột; không cần
.apply().chuyển cờ 0/1 thành văn bản
Ưu tiên
Series.map()hoặcreplace()cho các phép biến đổi theo phần tử trên một cột.Kiểm soát hình dạng đầu ra từ .apply()
Khi hàm của bạn trả về nhiều hơn một giá trị cho mỗi hàng, hãy làm rõ hình dạng. Điều này tránh bất ngờ và hỏng vỡ ở các phiên bản tương lai.
trả về một list và mở rộng thành các cột
Dùng
result_type='expand'để tách một list/mảng thành nhiều cột.trả về một series hoặc dict để tự động đặt tên cột
Trả về một
Serieshoặcdicttừ mỗi hàng sẽ tạo ra một DataFrame với nhãn cột tương ứng.Mẹo hiệu năng đáng lưu ý
.apply(axis=1)theo hàng rất tiện nhưng chậm trên các khung dữ liệu lớn vì nó gọi hàm Python của bạn một lần cho mỗi hàng. Các mẫu dưới đây giúp tránh nút thắt này.sum,mean,clip,where,astype, và các phương thức xử lý chuỗi/bộ truy cập.raw=Trueđể nhận mảng NumPy và giảm chi phí của pandas.Series.map()hoặc các phương thức vector hóa thay choDataFrame.apply(axis=1).Ghi chú phiên bản bạn nên biết (pandas 2.x)
Các bản phát hành pandas gần đây đã siết chặt hành vi quanh .apply() để kết quả dễ dự đoán hơn.
result_type='expand'khi bạn muốn nhiều cột.Serieshoặcdictsẽ được mở rộng thành một DataFrame với các khóa đó làm cột (ổn định từ 0.23).Series.apply()thay đổi: Tham sốconvert_dtypeđã bị loại bỏ. Nếu bạn cần kiểu hỗn hợp, hãy ép sangobjecttrước (ví dụ,s.astype("object").apply(fn)). Các chữ ký mới cho phép kiểm soát việcfnnhận vô hướng hay một Series trong nhiều ngữ cảnh hơn (ví dụ,by_row)..apply() so với .map() so với .applymap() so với .agg()
Hãy chọn API phù hợp với hình dạng của phép biến đổi bạn cần.
Series.map(func_or_dict): Biến đổi theo phần tử trên một cột. Tốt nhất cho tra cứu hoặc hàm đơn giản.DataFrame.apply(func, axis=1): Logic theo hàng cần nhiều cột.DataFrame.apply(func, axis=0): Logic theo cột (mỗi đầu vào là một Series đại diện cho một cột).DataFrame.applymap(func): Theo phần tử trên mọi ô. Dùng hạn chế; phương thức vector hóa nhanh hơn..agg()/.transform(): Tổng hợp và biến đổi theo nhóm; ưu tiên dùng trong các pipeline groupby.Lỗi thường gặp và cách khắc phục nhanh
Đây là những vấn đề thường khiến kết quả sai hoặc chậm, kèm cách sửa.
axis=1.result_type='expand'để tách thành các cột.raw=Trueđể giảm chi phí.astypenếu cần.Kết luận
.apply()là một công cụ linh hoạt, nhưng không phải lối tắt về hiệu năng. Hãy dùng các phép toán vector hóa cho số học, tổng hợp, và biến đổi theo phần tử trên một cột. Chỉ dùngDataFrame.apply(axis=1)khi logic của bạn thực sự cần nhiều cột trên mỗi hàng. Khi dùng, hãy kiểm soát hình dạng đầu ra bằngresult_type, cân nhắcraw=Truecho các hàm số, và chú ý đếndtypes. Những mẫu này cho kết quả có thể dự đoán trên pandas hiện đại và mở rộng tốt khi dữ liệu của bạn tăng lên.