Tự Học Data Science · 01/01/2024 0

03.12 High-Performance Pandas – eval() and query()

Truy vấn và đánh giá: Biểu thức kết hợp

Chúng ta đã thấy trước đây rằng NumPy và Pandas hỗ trợ các thao tác vector nhanh; ví dụ, khi thêm các phần tử của hai mảng:

import numpy as nprng = np.random.RandomState(42)x = rng.rand(1000000)y = rng.rand(1000000)%timeit x + y
100 loops, best of 3: 3.39 ms per loop

Như đã thảo luận trong Tính toán trên Mảng NumPy: Hàm Universal, việc này nhanh hơn nhiều so với việc thực hiện phép cộng thông qua vòng lặp hoặc phép toán comprehension của Python:

%timeit np.fromiter((xi + yi for xi, yi in zip(x, y)), dtype=x.dtype, count=len(x))
1 loop, best of 3: 266 ms per loop

Tuy nhiên, sự trừu tượng này có thể trở nên kém hiệu quả khi tính toán các biểu thức phức hợp.Ví dụ, hãy xem xét biểu thức sau đây:

mask = (x > 0.5) & (y < 0.5)

Vì NumPy đánh giá mỗi biểu thức con, điều này tương đương với sau:

tmp1 = (x > 0.5)tmp2 = (y < 0.5)mask = tmp1 & tmp2

Nói cách khác, mọi bước trung gian được phân bổ rõ ràng trong bộ nhớ. Nếu các mảng xy rất lớn, điều này có thể dẫn đến sự tăng không đáng kể về mặt bộ nhớ và tính toán.Thư viện Numexpr cho phép bạn tính toán loại biểu thức hỗn hợp này từng phần tử một, mà không cần phải phân bổ toàn bộ mảng trung gian.Tài liệu Numexpr cung cấp thêm chi tiết, nhưng trong lúc này chỉ cần nói rằng thư viện chấp nhận một chuỗi biểu thức theo kiểu NumPy mà bạn muốn tính toán:

import numexprmask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')np.allclose(mask, mask_numexpr)
True

Lợi ích ở đây là Numexpr đánh giá biểu thức mà không sử dụng các mảng tạm thời có kích thước đầy đủ, và do đó có thể hiệu quả hơn NumPy, đặc biệt là đối với các mảng lớn.Các công cụ eval()query() của Pandas mà chúng ta sẽ thảo luận ở đây có ý tưởng tương tự và phụ thuộc vào package Numexpr.

pandas.eval() cho Hiệu suất Hoạt động

Hàm eval() trong Pandas sử dụng biểu thức chuỗi để tính toán hiệu quả các hoạt động sử dụng DataFrames.Ví dụ, xem xét các DataFrame sau đây:

import pandas as pdnrows, ncols = 100000, 100rng = np.random.RandomState(42)df1, df2, df3, df4 = (pd.DataFrame(rng.rand(nrows, ncols))                      for i in range(4))

Để tính tổng của tất cả bốn DataFrame bằng cách sử dụng phương pháp thông thường của Pandas, chúng ta chỉ cần viết mã sau:

%timeit df1 + df2 + df3 + df4
10 loops, best of 3: 87.1 ms per loop

Kết quả tương tự có thể tính được thông qua pd.eval bằng cách xây dựng biểu thức dưới dạng chuỗi:

%timeit pd.eval('df1 + df2 + df3 + df4')
10 loops, best of 3: 42.2 ms per loop

Phiên bản eval() của biểu thức này nhanh khoảng 50% (và sử dụng ít bộ nhớ hơn), đồng thời đưa ra kết quả tương tự:

np.allclose(df1 + df2 + df3 + df4,            pd.eval('df1 + df2 + df3 + df4'))
True

Các hoạt động được hỗ trợ bởi pd.eval()

Kể từ phiên bản Pandas v0.16, pd.eval() hỗ trợ một loạt các hoạt động.

df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 1000, (100, 3)))                           for i in range(5))

Toán tử số học

Phương thức pd.eval() hỗ trợ tất cả các toán tử số học. Ví dụ:

result1 = -df1 * df2 / (df3 + df4) - df5result2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')np.allclose(result1, result2)
True

Toán tử so sánh

pd.eval() hỗ trợ tất cả các toán tử so sánh, bao gồm cả biểu thức dùng liên tiếp:

result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)result2 = pd.eval('df1 < df2 <= df3 != df4')np.allclose(result1, result2)
True

Toán tử bitwise

pd.eval() hỗ trợ các toán tử bit &|:

result1 = (df1 < 0.5) & (df2 < 0.5) | (df3 < df4)result2 = pd.eval('(df1 < 0.5) & (df2 < 0.5) | (df3 < df4)')np.allclose(result1, result2)
True

Ngoài ra, nó hỗ trợ sử dụng từ đúng nghĩa andor trong biểu thức Boolean:

result3 = pd.eval('(df1 < 0.5) and (df2 < 0.5) or (df3 < df4)')np.allclose(result1, result3)
True

Thuộc tính và chỉ số của đối tượng

pd.eval() hỗ trợ truy cập đến thuộc tính của đối tượng qua cú pháp obj.attr, và truy cập vào chỉ số qua cú pháp obj[index]:

result1 = df2.T[0] + df3.iloc[1]result2 = pd.eval('df2.T[0] + df3.iloc[1]')np.allclose(result1, result2)
True

Các hoạt động khác

Các hoạt động khác như gọi hàm, câu lệnh điều kiện, vòng lặp và những cấu trúc phức tạp khác hiện tại không được thực hiện trong pd.eval().Nếu bạn muốn thực hiện những biểu thức loại phức tạp hơn này, bạn có thể sử dụng thư viện Numexpr chính nó.

DataFrame.eval() cho các hoạt động theo cột

Giống như Pandas có một hàm cấp cao pd.eval(), các đối tượng DataFrame cũng có một phương thức eval() hoạt động tương tự.Lợi ích của phương thức eval() là các cột có thể được tham chiếu bằng tên.Chúng ta sẽ sử dụng mảng được gán nhãn này làm ví dụ:

df = pd.DataFrame(rng.rand(1000, 3), columns=['A', 'B', 'C'])df.head()

Sử dụng pd.eval() như trên, chúng ta có thể tính toán biểu thức với ba cột như sau:

result1 = (df['A'] + df['B']) / (df['C'] - 1)result2 = pd.eval("(df.A + df.B) / (df.C - 1)")np.allclose(result1, result2)
True

Phương thức DataFrame.eval() cho phép đánh giá một cách ngắn gọn các biểu thức với các cột:

result3 = df.eval('(A + B) / (C - 1)')np.allclose(result1, result3)
True

Lưu ý rằng chúng ta coi tên cột như biến trong biểu thức đã được đánh giá, và kết quả là những gì chúng ta mong muốn.

Gán giá trị trong DataFrame.eval()

Ngoài những tùy chọn vừa được thảo luận, DataFrame.eval() cũng cho phép gán giá trị cho bất kỳ cột nào.Hãy sử dụng DataFrame từ trước đó, có các cột 'A', 'B''C':

df.head()

Chúng ta có thể sử dụng df.eval() để tạo một cột mới 'D' và gán cho nó một giá trị được tính toán từ các cột khác:

df.eval('D = (A + B) / C', inplace=True)df.head()

Giống như vậy, bất kỳ cột hiện tại nào cũng có thể được chỉnh sửa:

df.eval('D = (A - B) / C', inplace=True)df.head()

Các biến cục bộ trong DataFrame.eval()

Phương thức DataFrame.eval() hỗ trợ cú pháp bổ sung cho phép nó làm việc với các biến Python cục bộ.Xem ví dụ sau:

column_mean = df.mean(1)result1 = df['A'] + column_meanresult2 = df.eval('A + @column_mean')np.allclose(result1, result2)
True

Ký tự @ ở đây đánh dấu một tên biến chứ không phải là một tên cột, và cho phép bạn đánh giá hiệu quả các biểu thức liên quan đến hai “không gian tên”: không gian tên các cột và không gian tên các đối tượng Python.Lưu ý rằng ký tự @ này chỉ được hỗ trợ bởi phương thức DataFrame.eval(), không được hỗ trợ bởi hàm pandas.eval(), bởi vì hàm pandas.eval() chỉ có quyền truy cập vào một (Python) không gian tên.

Phương thức DataFrame.query()

Trong thư viện pandas, DataFrame có một phương thức khác dựa trên chuỗi đã được đánh giá, được gọi là phương thức query(). Hãy xem ví dụ sau:

result1 = df[(df.A < 0.5) & (df.B < 0.5)]result2 = pd.eval('df[(df.A < 0.5) & (df.B < 0.5)]')np.allclose(result1, result2)
True

Như ví dụ sử dụng trong cuộc trò chuyện về DataFrame.eval(), đây là một biểu thức liên quan đến các cột của DataFrame.Nó không thể được biểu thị bằng cú pháp DataFrame.eval()!Thay vào đó, cho loại hoạt động lọc này, bạn có thể sử dụng phương thức query():

result2 = df.query('A < 0.5 and B < 0.5')np.allclose(result1, result2)
True

Ngoài việc tính toán hiệu quả hơn so với biểu thức che, điểm đặc biệt là ta dễ dàng đọc và hiểu hơn.Lưu ý rằng phương thức query() cũng chấp nhận cờ @ để đánh dấu các biến cục bộ:

Cmean = df['C'].mean()result1 = df[(df.A < Cmean) & (df.B < Cmean)]result2 = df.query('A < @Cmean and B < @Cmean')np.allclose(result1, result2)
True

Hiệu suất: Khi nào thì sử dụng các hàm này

Khi xem xét việc sử dụng các chức năng này, có hai yếu tố cần xem xét: thời gian tính toánsử dụng bộ nhớ.Sử dụng bộ nhớ là khía cạnh dễ dự đoán nhất. Như đã đề cập, mọi biểu thức phức hợp liên quan đến mảng NumPy hoặc DataFrame của Pandas sẽ dẫn đến việc tạo ra mảng tạm thời một cách ngầm định:Ví dụ, điều này:

x = df[(df.A < 0.5) & (df.B < 0.5)]

Một cách tương đương xấp xỉ như sau:

tmp1 = df.A < 0.5tmp2 = df.B < 0.5tmp3 = tmp1 & tmp2x = df[tmp3]

Nếu kích thước của các DataFrame tạm thời lớn so với bộ nhớ hệ thống có sẵn của bạn (thường là vài gigabyte) thì việc sử dụng biểu thức eval() hoặc query() là một ý tưởng tốt.Bạn có thể kiểm tra kích thước gần đúng của mảng của bạn theo đơn vị byte bằng cách sử dụng câu lệnh này:

df.values.nbytes
32000

Về mặt hiệu suất, eval() có thể nhanh hơn ngay cả khi bạn không dùng hết bộ nhớ hệ thống.

Chúng ta đã đề cập tới hầu hết các chi tiết của eval()query() ở đây; để biết thêm thông tin về chúng, bạn có thể tham khảo tài liệu Pandas.Đặc biệt, người dùng có thể chỉ định các trình phân tích cú pháp và công cụ cụ thể cho việc chạy các truy vấn này; để biết thông tin chi tiết về điều này, hãy xem phần thảo luận trong phần “Tăng cường Hiệu suất”.