TLDR; Không, for
vòng lặp không phải là "xấu", ít nhất, không phải lúc nào cũng vậy. Có lẽ chính xác hơn khi nói rằng một số hoạt động được vector hóa chậm hơn so với lặp lại , so với việc nói rằng lặp lại nhanh hơn một số hoạt động được vector hóa. Biết khi nào và tại sao là chìa khóa để đạt được hiệu suất cao nhất từ mã của bạn. Tóm lại, đây là những tình huống đáng xem xét một giải pháp thay thế cho các chức năng của gấu trúc vectơ:
- Khi dữ liệu của bạn nhỏ (... tùy thuộc vào việc bạn đang làm),
- Khi xử lý
object
/ hỗn hợp các loại
- Khi sử dụng các
str
chức năng của trình truy cập / regex
Hãy xem xét những tình huống này riêng lẻ.
Lặp lại v / s Vectơ hóa trên dữ liệu nhỏ
Pandas tuân theo cách tiếp cận "Quy ước qua cấu hình" trong thiết kế API của nó. Điều này có nghĩa là cùng một API đã được trang bị để phục vụ cho nhiều loại dữ liệu và trường hợp sử dụng.
Khi một hàm pandas được gọi, những điều sau (trong số những thứ khác) phải được hàm xử lý nội bộ, để đảm bảo hoạt động
- Căn chỉnh chỉ mục / trục
- Xử lý các kiểu dữ liệu hỗn hợp
- Xử lý dữ liệu bị thiếu
Hầu hết mọi chức năng sẽ phải xử lý những điều này ở các mức độ khác nhau và điều này thể hiện một khoản chi phí . Chi phí ít hơn cho các hàm số (ví dụ, Series.add
), trong khi nó rõ ràng hơn cho các hàm chuỗi (ví dụ, Series.str.replace
).
for
Mặt khác, các vòng lặp nhanh hơn bạn nghĩ. Điều tốt hơn nữa là khả năng hiểu danh sách (tạo danh sách thông qua for
các vòng lặp) thậm chí còn nhanh hơn vì chúng là cơ chế lặp lại được tối ưu hóa để tạo danh sách.
Danh sách hiểu theo mẫu
[f(x) for x in seq]
Trong trường hợp seq
là một loạt gấu trúc hoặc cột DataFrame. Hoặc, khi thao tác trên nhiều cột,
[f(x, y) for x, y in zip(seq1, seq2)]
Cột ở đâu seq1
và ở đâu seq2
.
So sánh số
Hãy xem xét một thao tác lập chỉ mục boolean đơn giản. Phương thức hiểu danh sách đã được tính thời gian đối với Series.ne
( !=
) và query
. Đây là các chức năng:
# Boolean indexing with Numeric value comparison.
df[df.A != df.B] # vectorized !=
df.query('A != B') # query (numexpr)
df[[x != y for x, y in zip(df.A, df.B)]] # list comp
Để đơn giản, tôi đã sử dụng perfplot
gói để chạy tất cả các bài kiểm tra thời gian trong bài đăng này. Dưới đây là thời gian cho các hoạt động trên:
Khả năng hiểu danh sách hoạt động tốt hơn query
đối với N có kích thước vừa phải, và thậm chí còn tốt hơn so với so sánh không bằng vectơ đối với N. Thật không may, khả năng hiểu danh sách quy mô tuyến tính, vì vậy nó không mang lại nhiều hiệu suất cho N lớn hơn.
Lưu ý
Điều đáng nói là phần lớn lợi ích của việc hiểu danh sách đến từ việc không phải lo lắng về căn chỉnh chỉ mục, nhưng điều này có nghĩa là nếu mã của bạn phụ thuộc vào căn chỉnh lập chỉ mục, điều này sẽ bị hỏng. Trong một số trường hợp, các hoạt động vectorised trên các mảng NumPy cơ bản có thể được coi là mang lại "điều tốt nhất của cả hai thế giới", cho phép vectorisation mà không cần tất cả các chức năng không cần thiết của các hàm gấu trúc. Điều này có nghĩa là bạn có thể viết lại thao tác trên dưới dạng
df[df.A.values != df.B.values]
Điều nào vượt trội hơn cả gấu trúc và tương đương về khả năng hiểu danh sách:
Vectơ hóa NumPy nằm ngoài phạm vi của bài đăng này, nhưng nó chắc chắn đáng xem xét, nếu hiệu suất quan trọng.
Đếm giá trị
Lấy một ví dụ khác - lần này, với một cấu trúc python vani khác nhanh hơn vòng lặp for - collections.Counter
. Một yêu cầu phổ biến là tính toán số lượng giá trị và trả về kết quả dưới dạng từ điển. Này được thực hiện với value_counts
, np.unique
và Counter
:
# Value Counts comparison.
ser.value_counts(sort=False).to_dict() # value_counts
dict(zip(*np.unique(ser, return_counts=True))) # np.unique
Counter(ser) # Counter
Kết quả rõ ràng hơn, Counter
vượt trội hơn cả hai phương pháp vector hóa cho phạm vi lớn hơn của N nhỏ (~ 3500).
Lưu ý
Câu đố khác (nhã nhặn @ user2357112). Các Counter
được thực hiện với một tốc C , vì vậy trong khi nó vẫn phải làm việc với các đối tượng python thay cho kiểu dữ liệu C cơ bản, nó vẫn là nhanh hơn so với một for
vòng lặp. Sức mạnh Python!
Tất nhiên, điều cần lưu ý ở đây là hiệu suất phụ thuộc vào dữ liệu và trường hợp sử dụng của bạn. Mục đích của những ví dụ này là thuyết phục bạn không loại trừ những giải pháp này là những lựa chọn hợp pháp. Nếu những điều này vẫn không mang lại cho bạn hiệu suất bạn cần, luôn có cython và numba . Hãy thêm bài kiểm tra này vào hỗn hợp.
from numba import njit, prange
@njit(parallel=True)
def get_mask(x, y):
result = [False] * len(x)
for i in prange(len(x)):
result[i] = x[i] != y[i]
return np.array(result)
df[get_mask(df.A.values, df.B.values)] # numba
Numba cung cấp biên dịch JIT của mã python lặp lại thành mã được vector hóa rất mạnh mẽ. Hiểu cách làm cho numba hoạt động liên quan đến một đường cong học tập.
Hoạt động với hỗn hợp / object
loại
So sánh dựa trên chuỗi
Xem lại ví dụ lọc từ phần đầu tiên, điều gì sẽ xảy ra nếu các cột được so sánh là chuỗi? Hãy xem xét 3 hàm tương tự ở trên, nhưng với DataFrame đầu vào được ép kiểu thành chuỗi.
# Boolean indexing with string value comparison.
df[df.A != df.B] # vectorized !=
df.query('A != B') # query (numexpr)
df[[x != y for x, y in zip(df.A, df.B)]] # list comp
Vì vậy, những gì đã thay đổi? Điều cần lưu ý ở đây là các hoạt động chuỗi vốn đã rất khó để vectơ hóa. Pandas coi các chuỗi như các đối tượng và tất cả các hoạt động trên các đối tượng trở về trạng thái triển khai chậm, lặp lại.
Bây giờ, bởi vì việc triển khai lặp lại này được bao quanh bởi tất cả chi phí được đề cập ở trên, có sự khác biệt về độ lớn không đổi giữa các giải pháp này, mặc dù chúng có quy mô như nhau.
Khi nói đến các hoạt động trên các đối tượng có thể thay đổi / phức tạp, không có sự so sánh. Khả năng hiểu danh sách vượt trội hơn tất cả các hoạt động liên quan đến số và danh sách.
Truy cập (các) Giá trị Từ điển bằng Khóa
Đây là thời gian cho hai thao tác trích xuất một giá trị từ một cột từ điển: map
và đọc hiểu danh sách. Thiết lập nằm trong Phụ lục, dưới tiêu đề "Đoạn mã".
# Dictionary value extraction.
ser.map(operator.itemgetter('value')) # map
pd.Series([x.get('value') for x in ser]) # list comprehension
Vị trí Danh sách Indexing
Thời gian cho 3 hoạt động mà trích xuất các yếu tố 0 từ một danh sách các cột (xử lý ngoại lệ), map
, str.get
accessor phương pháp , và hiểu danh sách:
# List positional indexing.
def get_0th(lst):
try:
return lst[0]
# Handle empty lists and NaNs gracefully.
except (IndexError, TypeError):
return np.nan
ser.map(get_0th) # map
ser.str[0] # str accessor
pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]) # list comp
pd.Series([get_0th(x) for x in ser]) # list comp safe
Lưu ý
Nếu chỉ mục quan trọng, bạn sẽ muốn thực hiện:
pd.Series([...], index=ser.index)
Khi dựng lại bộ truyện.
Làm phẳng danh sách
Một ví dụ cuối cùng là làm phẳng danh sách. Đây là một vấn đề phổ biến khác và chứng minh rằng python thuần túy mạnh đến mức nào ở đây.
# Nested list flattening.
pd.DataFrame(ser.tolist()).stack().reset_index(drop=True) # stack
pd.Series(list(chain.from_iterable(ser.tolist()))) # itertools.chain
pd.Series([y for x in ser for y in x]) # nested list comp
Cả hai itertools.chain.from_iterable
và khả năng hiểu danh sách lồng nhau đều là cấu trúc python thuần túy và quy mô tốt hơn nhiều so với stack
giải pháp.
Các mốc thời gian này là một dấu hiệu mạnh mẽ cho thấy rằng gấu trúc không được trang bị để làm việc với các loại hỗn hợp và có lẽ bạn nên hạn chế sử dụng nó để làm như vậy. Bất cứ khi nào có thể, dữ liệu phải được hiển thị dưới dạng giá trị vô hướng (ints / float / string) trong các cột riêng biệt.
Cuối cùng, khả năng áp dụng của các giải pháp này phụ thuộc nhiều vào dữ liệu của bạn. Vì vậy, điều tốt nhất nên làm là kiểm tra các hoạt động này trên dữ liệu của bạn trước khi quyết định điều gì sẽ xảy ra. Lưu ý rằng tôi đã không tính thời gian apply
cho các giải pháp này, vì nó sẽ làm sai lệch đồ thị (vâng, nó chậm như vậy).
Thao tác Regex và .str
Phương thức truy cập
Pandas có thể áp dụng các hoạt động regex như str.contains
, str.extract
và str.extractall
, cũng như các hoạt động chuỗi " str.split
vectơ hóa" khác (chẳng hạn như , str.find ,
str.translate`, v.v.) trên các cột chuỗi. Các chức năng này chậm hơn so với khả năng hiểu danh sách và có nghĩa là các chức năng tiện lợi hơn bất kỳ chức năng nào khác.
Việc biên dịch trước một mẫu regex và lặp qua dữ liệu của bạn với nó thường nhanh hơn nhiều re.compile
(xem thêm Liệu có đáng sử dụng biên dịch lại của Python không? ). Danh sách comp tương đương str.contains
trông giống như sau:
p = re.compile(...)
ser2 = pd.Series([x for x in ser if p.search(x)])
Hoặc là,
ser2 = ser[[bool(p.search(x)) for x in ser]]
Nếu bạn cần xử lý NaN, bạn có thể làm điều gì đó như
ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]]
Danh sách comp tương đương với str.extract
(không có nhóm) sẽ giống như sau:
df['col2'] = [p.search(x).group(0) for x in df['col']]
Nếu bạn cần xử lý không khớp và NaN, bạn có thể sử dụng chức năng tùy chỉnh (vẫn nhanh hơn!):
def matcher(x):
m = p.search(str(x))
if m:
return m.group(0)
return np.nan
df['col2'] = [matcher(x) for x in df['col']]
Các matcher
chức năng là rất mở rộng. Nó có thể được trang bị để trả về danh sách cho mỗi nhóm chụp, nếu cần. Chỉ cần trích xuất truy vấn group
hoặc groups
thuộc tính của đối tượng so khớp.
Đối với str.extractall
, thay đổi p.search
thành p.findall
.
Trích xuất chuỗi
Hãy xem xét một hoạt động lọc đơn giản. Ý tưởng là trích xuất 4 chữ số nếu nó đứng trước một chữ cái viết hoa.
# Extracting strings.
p = re.compile(r'(?<=[A-Z])(\d{4})')
def matcher(x):
m = p.search(x)
if m:
return m.group(0)
return np.nan
ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False) # str.extract
pd.Series([matcher(x) for x in ser]) # list comprehension
Các ví dụ khác
Tiết lộ đầy đủ - Tôi là tác giả (một phần hoặc toàn bộ) của những bài đăng này được liệt kê bên dưới.
Phần kết luận
Như được hiển thị từ các ví dụ trên, lặp lại tỏa sáng khi làm việc với các hàng nhỏ của DataFrames, kiểu dữ liệu hỗn hợp và biểu thức chính quy.
Việc tăng tốc bạn nhận được tùy thuộc vào dữ liệu và vấn đề của bạn, vì vậy quãng đường của bạn có thể khác nhau. Điều tốt nhất cần làm là cẩn thận chạy các bài kiểm tra và xem liệu khoản thanh toán có xứng đáng với nỗ lực không.
Các chức năng "vectơ hóa" thể hiện sự đơn giản và dễ đọc của chúng, vì vậy nếu hiệu suất không quan trọng, bạn chắc chắn nên thích những chức năng đó.
Một lưu ý phụ khác, các hoạt động chuỗi nhất định giải quyết các ràng buộc có lợi cho việc sử dụng NumPy. Dưới đây là hai ví dụ trong đó vectơ hóa NumPy cẩn thận tốt hơn python:
Ngoài ra, đôi khi chỉ hoạt động trên các mảng bên dưới .values
chứ không phải trên Series hoặc DataFrames có thể cung cấp tốc độ đủ mạnh cho hầu hết các trường hợp thông thường (xem Lưu ý trong phần So sánh số ở trên). Vì vậy, ví dụ df[df.A.values != df.B.values]
sẽ hiển thị hiệu suất tăng nhanh hơn df[df.A != df.B]
. Việc sử dụng .values
có thể không phù hợp trong mọi tình huống, nhưng đó là một thủ thuật hữu ích cần biết.
Như đã đề cập ở trên, bạn quyết định xem liệu những giải pháp này có đáng để bạn gặp khó khăn khi thực hiện hay không.
Phụ lục: Đoạn mã
import perfplot
import operator
import pandas as pd
import numpy as np
import re
from collections import Counter
from itertools import chain
# Boolean indexing with Numeric value comparison.
perfplot.show(
setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B']),
kernels=[
lambda df: df[df.A != df.B],
lambda df: df.query('A != B'),
lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
lambda df: df[get_mask(df.A.values, df.B.values)]
],
labels=['vectorized !=', 'query (numexpr)', 'list comp', 'numba'],
n_range=[2**k for k in range(0, 15)],
xlabel='N'
)
# Value Counts comparison.
perfplot.show(
setup=lambda n: pd.Series(np.random.choice(1000, n)),
kernels=[
lambda ser: ser.value_counts(sort=False).to_dict(),
lambda ser: dict(zip(*np.unique(ser, return_counts=True))),
lambda ser: Counter(ser),
],
labels=['value_counts', 'np.unique', 'Counter'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=lambda x, y: dict(x) == dict(y)
)
# Boolean indexing with string value comparison.
perfplot.show(
setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B'], dtype=str),
kernels=[
lambda df: df[df.A != df.B],
lambda df: df.query('A != B'),
lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
],
labels=['vectorized !=', 'query (numexpr)', 'list comp'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Dictionary value extraction.
ser1 = pd.Series([{'key': 'abc', 'value': 123}, {'key': 'xyz', 'value': 456}])
perfplot.show(
setup=lambda n: pd.concat([ser1] * n, ignore_index=True),
kernels=[
lambda ser: ser.map(operator.itemgetter('value')),
lambda ser: pd.Series([x.get('value') for x in ser]),
],
labels=['map', 'list comprehension'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# List positional indexing.
ser2 = pd.Series([['a', 'b', 'c'], [1, 2], []])
perfplot.show(
setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
kernels=[
lambda ser: ser.map(get_0th),
lambda ser: ser.str[0],
lambda ser: pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]),
lambda ser: pd.Series([get_0th(x) for x in ser]),
],
labels=['map', 'str accessor', 'list comprehension', 'list comp safe'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Nested list flattening.
ser3 = pd.Series([['a', 'b', 'c'], ['d', 'e'], ['f', 'g']])
perfplot.show(
setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
kernels=[
lambda ser: pd.DataFrame(ser.tolist()).stack().reset_index(drop=True),
lambda ser: pd.Series(list(chain.from_iterable(ser.tolist()))),
lambda ser: pd.Series([y for x in ser for y in x]),
],
labels=['stack', 'itertools.chain', 'nested list comp'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Extracting strings.
ser4 = pd.Series(['foo xyz', 'test A1234', 'D3345 xtz'])
perfplot.show(
setup=lambda n: pd.concat([ser4] * n, ignore_index=True),
kernels=[
lambda ser: ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False),
lambda ser: pd.Series([matcher(x) for x in ser])
],
labels=['str.extract', 'list comprehension'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
pd.Series
vàpd.DataFrame
hiện hỗ trợ xây dựng từ các mục lặp. Điều đó có nghĩa là người ta có thể chỉ cần chuyển một trình tạo Python cho các hàm khởi tạo thay vì cần phải xây dựng một danh sách trước (sử dụng khả năng hiểu danh sách), điều này có thể chậm hơn trong nhiều trường hợp. Tuy nhiên, kích thước của đầu ra máy phát điện không thể được xác định trước. Tôi không chắc điều đó sẽ gây ra bao nhiêu thời gian / bộ nhớ.