TLDR; ไม่for
ลูปไม่ใช่ผ้าห่ม "ไม่ดี" อย่างน้อยก็ไม่เสมอไป อาจจะแม่นยำกว่าที่จะบอกว่าการดำเนินการแบบเวกเตอร์บางอย่างช้ากว่าการทำซ้ำเมื่อเทียบกับการบอกว่าการวนซ้ำเร็วกว่าการดำเนินการแบบเวกเตอร์บางอย่าง การรู้ว่าเมื่อไรและเหตุใดจึงเป็นกุญแจสำคัญในการทำให้โค้ดของคุณมีประสิทธิภาพสูงสุด โดยสรุปนี่คือสถานการณ์ที่ควรค่าแก่การพิจารณาทางเลือกอื่นสำหรับฟังก์ชันแพนด้าแบบเวกเตอร์:
- เมื่อข้อมูลของคุณมีขนาดเล็ก (... ขึ้นอยู่กับสิ่งที่คุณกำลังทำอยู่)
- เมื่อจัดการกับ
object
/ ผสม dtypes
- เมื่อใช้ฟังก์ชันการเข้าถึง
str
/ regex
ลองตรวจสอบสถานการณ์เหล่านี้ทีละรายการ
การทำซ้ำ v / s Vectorization บนข้อมูลขนาดเล็ก
Pandas ปฏิบัติตามแนวทาง"Convention Over Configuration"ในการออกแบบ API ซึ่งหมายความว่า API เดียวกันได้รับการติดตั้งเพื่อรองรับข้อมูลและกรณีการใช้งานที่หลากหลาย
เมื่อมีการเรียกใช้ฟังก์ชันแพนด้าฟังก์ชันต่อไปนี้จะต้องได้รับการจัดการภายในเพื่อให้แน่ใจว่าทำงานได้
- การจัดแนวดัชนี / แกน
- การจัดการประเภทข้อมูลผสม
- การจัดการข้อมูลที่ขาดหายไป
ฟังก์ชั่นเกือบทุกคนจะต้องจัดการกับเหล่านี้เพื่อขอบเขตที่แตกต่างกันและนำเสนอนี้ค่าใช้จ่าย ค่าโสหุ้ยมีค่าน้อยกว่าสำหรับฟังก์ชันตัวเลข (เช่นSeries.add
) ในขณะที่ฟังก์ชันสตริงจะเด่นชัดกว่า (เช่นSeries.str.replace
)
for
ในทางกลับกันลูปเร็วกว่าที่คุณคิด สิ่งที่ดีไปกว่านั้นคือการทำความเข้าใจรายการ (ซึ่งสร้างรายการผ่านfor
ลูป) จะเร็วกว่าเนื่องจากมีการปรับกลไกการทำซ้ำสำหรับการสร้างรายการ
ความเข้าใจในรายการเป็นไปตามรูปแบบ
[f(x) for x in seq]
seq
ชุดแพนด้าหรือคอลัมน์ DataFrame อยู่ที่ไหน หรือเมื่อทำงานในหลายคอลัมน์
[f(x, y) for x, y in zip(seq1, seq2)]
คอลัมน์ที่ไหนseq1
และอยู่ที่ไหนseq2
การเปรียบเทียบตัวเลข
พิจารณาการดำเนินการจัดทำดัชนีบูลีนอย่างง่าย วิธีรายการความเข้าใจที่ได้รับการหมดเวลากับSeries.ne
( !=
) query
และ นี่คือฟังก์ชั่น:
df[df.A != df.B]
df.query('A != B')
df[[x != y for x, y in zip(df.A, df.B)]]
เพื่อความง่ายฉันได้ใช้perfplot
แพ็คเกจเพื่อเรียกใช้การทดสอบตลอดเวลาในโพสต์นี้ กำหนดเวลาสำหรับการดำเนินการข้างต้นอยู่ด้านล่าง:
ความเข้าใจในรายการมีประสิทธิภาพสูงกว่าquery
N ที่มีขนาดปานกลางและยังมีประสิทธิภาพดีกว่าการเปรียบเทียบเวกเตอร์ที่ไม่เท่ากับการเปรียบเทียบสำหรับ N ขนาดเล็ก แต่น่าเสียดายที่ความเข้าใจในรายการจะปรับขนาดเป็นเชิงเส้นดังนั้นจึงไม่ได้ให้ประสิทธิภาพที่เพิ่มขึ้นมากสำหรับ N ที่ใหญ่กว่า
หมายเหตุ
เป็นสิ่งที่ควรค่าแก่การกล่าวถึงประโยชน์ส่วนใหญ่ของการทำความเข้าใจรายการมาจากการที่คุณไม่ต้องกังวลเกี่ยวกับการจัดแนวดัชนี แต่หมายความว่าหากรหัสของคุณขึ้นอยู่กับการจัดแนวดัชนีสิ่งนี้จะเสียหาย ในบางกรณีการดำเนินการ vectorised บนอาร์เรย์ NumPy ที่อยู่ข้างใต้ถือได้ว่าเป็นการนำ "สิ่งที่ดีที่สุดของทั้งสองโลก" มาใช้ทำให้สามารถสร้างเวกเตอร์ได้โดยไม่ต้องมีค่าใช้จ่ายที่ไม่จำเป็นทั้งหมดของฟังก์ชันแพนด้า ซึ่งหมายความว่าคุณสามารถเขียนการดำเนินการด้านบนใหม่เป็น
df[df.A.values != df.B.values]
ซึ่งมีประสิทธิภาพดีกว่าทั้งแพนด้าและรายการที่เทียบเท่า:
การทำให้เป็นเวกเตอร์ NumPy อยู่นอกขอบเขตของโพสต์นี้ แต่ควรพิจารณาอย่างแน่นอนหากประสิทธิภาพมีความสำคัญ
มูลค่านับ
การอีกตัวอย่างหนึ่ง - ในขณะนี้อีกด้วยสร้างหลามวานิลลาที่เป็นได้เร็วขึ้นกว่าสำหรับวง collections.Counter
- ข้อกำหนดทั่วไปคือการคำนวณการนับค่าและส่งคืนผลลัพธ์เป็นพจนานุกรม นี้จะกระทำด้วยvalue_counts
, np.unique
และCounter
:
ser.value_counts(sort=False).to_dict()
dict(zip(*np.unique(ser, return_counts=True)))
Counter(ser)
ผลลัพธ์จะเด่นชัดมากขึ้นCounter
ชนะทั้งสองวิธีแบบเวกเตอร์สำหรับช่วงขนาดเล็กที่ใหญ่กว่า N (~ 3500)
หมายเหตุ
เรื่องไม่สำคัญเพิ่มเติม (มารยาท @ user2357112) Counter
จะดำเนินการกับคันเร่ง Cดังนั้นในขณะที่มันยังคงมีการทำงานร่วมกับงูหลามวัตถุแทนของประเภทข้อมูล C พื้นฐานก็ยังคงเป็นเร็วกว่าfor
ห่วง พลังงูเหลือม!
แน่นอนว่าสิ่งที่ต้องทำก็คือประสิทธิภาพขึ้นอยู่กับข้อมูลและกรณีการใช้งานของคุณ ประเด็นของตัวอย่างเหล่านี้คือการโน้มน้าวให้คุณไม่คิดว่าโซลูชันเหล่านี้เป็นตัวเลือกที่ถูกต้อง หากสิ่งเหล่านี้ยังไม่ให้ประสิทธิภาพที่คุณต้องการก็มีcythonและnumbaอยู่เสมอ ลองเพิ่มการทดสอบนี้ลงในส่วนผสม
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 นำเสนอการรวบรวม JIT ของรหัสหลามแบบวนซ้ำเป็นรหัสเวกเตอร์ที่ทรงพลังมาก การทำความเข้าใจวิธีการทำงานของ numba เกี่ยวข้องกับช่วงการเรียนรู้
การทำงานกับ Mixed / object
dtypes
การเปรียบเทียบแบบใช้สตริงการ
ทบทวนตัวอย่างการกรองจากส่วนแรกจะเกิดอะไรขึ้นถ้าคอลัมน์ที่กำลังเปรียบเทียบเป็นสตริง พิจารณา 3 ฟังก์ชั่นเดียวกันข้างต้น แต่ด้วย DataFrame ที่ป้อนข้อมูลเป็นสตริง
df[df.A != df.B]
df.query('A != B')
df[[x != y for x, y in zip(df.A, df.B)]]
มีอะไรเปลี่ยนแปลงบ้าง? สิ่งที่ควรทราบก็คือการดำเนินการของสตริงนั้นยากที่จะทำให้เป็นเวกเตอร์ได้ หมีแพนด้าถือว่าสตริงเป็นวัตถุและการดำเนินการทั้งหมดกับอ็อบเจกต์กลับเป็นการใช้งานแบบวนซ้ำที่ช้า
ตอนนี้เนื่องจากการใช้งานแบบวนรอบนี้ล้อมรอบด้วยค่าโสหุ้ยทั้งหมดที่กล่าวมาข้างต้นจึงมีความแตกต่างของขนาดคงที่ระหว่างโซลูชันเหล่านี้แม้ว่าจะมีขนาดเท่ากันก็ตาม
เมื่อพูดถึงการดำเนินการกับวัตถุที่เปลี่ยนแปลงไม่ได้ / ซับซ้อนจะไม่มีการเปรียบเทียบ ความเข้าใจในรายการมีประสิทธิภาพสูงกว่าการดำเนินการทั้งหมดที่เกี่ยวข้องกับคำสั่งและรายการ
การเข้าถึงค่าพจนานุกรมโดยคีย์
นี่คือการกำหนดเวลาสำหรับการดำเนินการสองรายการที่ดึงค่าจากคอลัมน์ของพจนานุกรม: map
และความเข้าใจรายการ การตั้งค่าอยู่ในภาคผนวกภายใต้หัวข้อ "ข้อมูลโค้ด"
ser.map(operator.itemgetter('value'))
pd.Series([x.get('value') for x in ser])
ตำแหน่งรายการการจัดทำดัชนี
การจับเวลา 3 การดำเนินงานที่แยกองค์ประกอบ 0 จากรายการของคอลัมน์ (การจัดการข้อยกเว้น) ซึ่งเป็นmap
, str.get
วิธีการเข้าถึงและเข้าใจรายการ:
def get_0th(lst):
try:
return lst[0]
except (IndexError, TypeError):
return np.nan
ser.map(get_0th)
ser.str[0]
pd.Series([x[0] if len(x) > 0 else np.nan for x in ser])
pd.Series([get_0th(x) for x in ser])
หมายเหตุ
หากดัชนีมีความสำคัญคุณต้องทำ:
pd.Series([...], index=ser.index)
เมื่อสร้างชุดใหม่
List Flattening
ตัวอย่างสุดท้ายคือการแบนรายการ นี่เป็นอีกหนึ่งปัญหาที่พบบ่อยและแสดงให้เห็นว่าไพ ธ อนบริสุทธิ์มีประสิทธิภาพเพียงใด
pd.DataFrame(ser.tolist()).stack().reset_index(drop=True)
pd.Series(list(chain.from_iterable(ser.tolist())))
pd.Series([y for x in ser for y in x])
itertools.chain.from_iterable
ความเข้าใจในรายการที่ซ้อนกันทั้งคู่เป็นโครงสร้าง python ที่บริสุทธิ์และปรับขนาดได้ดีกว่าstack
โซลูชันมาก
การกำหนดเวลาเหล่านี้เป็นข้อบ่งชี้ที่ชัดเจนถึงข้อเท็จจริงที่ว่าแพนด้าไม่ได้มีอุปกรณ์ที่จะทำงานร่วมกับ dtypes แบบผสมและคุณควรละเว้นจากการใช้เพื่อทำเช่นนั้น หากเป็นไปได้ข้อมูลควรแสดงเป็นค่าสเกลาร์ (ints / floats / strings) ในคอลัมน์แยกกัน
ประการสุดท้ายการบังคับใช้โซลูชันเหล่านี้ขึ้นอยู่กับข้อมูลของคุณอย่างกว้างขวาง ดังนั้นสิ่งที่ดีที่สุดที่ควรทำคือทดสอบการดำเนินการเหล่านี้กับข้อมูลของคุณก่อนตัดสินใจว่าจะใช้อะไร สังเกตว่าฉันไม่ได้จับเวลาapply
ในการแก้ปัญหาเหล่านี้เพราะมันจะทำให้กราฟบิดเบี้ยว (ใช่มันช้ามาก)
Regex Operations และ.str
Accessor Methods
นุ่นสามารถนำไปใช้ดำเนินงาน regex เช่นstr.contains
, str.extract
และstr.extractall
เช่นเดียวกับคนอื่น ๆ "vectorized" การดำเนินงานสตริง (เช่นstr.split
, str.find ,
str.translate`, และอื่น ๆ ) ในคอลัมน์สตริง ฟังก์ชั่นเหล่านี้ช้ากว่าการทำความเข้าใจรายการและหมายถึงฟังก์ชันอำนวยความสะดวกมากกว่าสิ่งอื่นใด
โดยปกติแล้วการคอมไพล์รูปแบบ regex ล่วงหน้าจะเร็วกว่ามากและทำซ้ำบนข้อมูลของคุณด้วยre.compile
(ดูที่การใช้ re.compile ของ Python คุ้มค่าหรือไม่ ) รายการเทียบเท่าคอมพ์str.contains
มีลักษณะดังนี้:
p = re.compile(...)
ser2 = pd.Series([x for x in ser if p.search(x)])
หรือ,
ser2 = ser[[bool(p.search(x)) for x in ser]]
หากคุณต้องการจัดการ NaN คุณสามารถทำสิ่งต่างๆเช่น
ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]]
รายการที่เทียบเท่ากับstr.extract
(ไม่มีกลุ่ม) จะมีลักษณะดังนี้:
df['col2'] = [p.search(x).group(0) for x in df['col']]
หากคุณต้องการจัดการกับการไม่ตรงกันและ NaN คุณสามารถใช้ฟังก์ชันที่กำหนดเองได้ (ยังเร็วกว่า!):
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']]
matcher
ฟังก์ชั่นจะขยายมาก สามารถติดตั้งเพื่อส่งคืนรายการสำหรับแต่ละกลุ่มการจับภาพได้ตามต้องการ เพียงแค่แยกการสืบค้นgroup
หรือgroups
แอตทริบิวต์ของวัตถุที่ตรงกัน
สำหรับstr.extractall
เปลี่ยนp.search
เป็นp.findall
.
การแยกสตริง
พิจารณาการกรองแบบง่าย แนวคิดคือการแยกตัวเลข 4 หลักหากนำหน้าด้วยตัวอักษรตัวพิมพ์ใหญ่
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)
pd.Series([matcher(x) for x in ser])
ตัวอย่างเพิ่มเติม
การเปิดเผยข้อมูลทั้งหมด - ฉันเป็นผู้เขียน (บางส่วนหรือทั้งหมด) ของโพสต์เหล่านี้ตามรายการด้านล่าง
สรุป
ดังที่แสดงจากตัวอย่างด้านบนการวนซ้ำจะส่องสว่างเมื่อทำงานกับแถวเล็ก ๆ ของ DataFrames ประเภทข้อมูลผสมและนิพจน์ทั่วไป
ความเร็วที่คุณได้รับขึ้นอยู่กับข้อมูลและปัญหาของคุณดังนั้นระยะทางของคุณอาจแตกต่างกันไป สิ่งที่ดีที่สุดที่ต้องทำคือเรียกใช้การทดสอบอย่างรอบคอบและดูว่าการจ่ายเงินนั้นคุ้มค่ากับความพยายามหรือไม่
ฟังก์ชัน "vectorized" เปล่งประกายในความเรียบง่ายและความสามารถในการอ่านดังนั้นหากประสิทธิภาพไม่สำคัญคุณควรเลือกฟังก์ชั่นเหล่านี้อย่างแน่นอน
หมายเหตุด้านอื่นการดำเนินการสตริงบางอย่างเกี่ยวข้องกับข้อ จำกัด ที่สนับสนุนการใช้ NumPy ต่อไปนี้เป็นตัวอย่างสองตัวอย่างที่ทำให้เวกเตอร์ NumPy มีประสิทธิภาพดีกว่า python:
นอกจากนี้บางครั้งการทำงานบนอาร์เรย์พื้นฐานผ่านทาง.values
ตรงข้ามกับซีรี่ส์หรือ DataFrames สามารถให้ความเร็วที่ดีพอสำหรับสถานการณ์ปกติส่วนใหญ่ (ดูหมายเหตุในส่วนการเปรียบเทียบตัวเลขด้านบน) ดังนั้นสำหรับตัวอย่างเช่นจะแสดงช่วยเพิ่มประสิทธิภาพการทำงานทันทีผ่านทางdf[df.A.values != df.B.values]
df[df.A != df.B]
การใช้.values
อาจไม่เหมาะสมในทุกสถานการณ์ แต่เป็นประโยชน์ที่ควรทราบ
ดังที่กล่าวไว้ข้างต้นขึ้นอยู่กับคุณที่จะตัดสินใจว่าโซลูชันเหล่านี้คุ้มค่ากับปัญหาในการนำไปใช้หรือไม่
ภาคผนวก: ข้อมูลโค้ด
import perfplot
import operator
import pandas as pd
import numpy as np
import re
from collections import Counter
from itertools import chain
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'
)
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)
)
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
)
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
)
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
)
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
)
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
และpd.DataFrame
ตอนนี้รองรับการสร้างจาก iterables นั่นหมายความว่าเราสามารถส่ง Python generator ไปยังฟังก์ชัน constructor ได้แทนที่จะต้องสร้างรายการขึ้นมาก่อน (โดยใช้ list comp understandions) ซึ่งอาจช้ากว่าในหลาย ๆ กรณี อย่างไรก็ตามไม่สามารถกำหนดขนาดของเอาต์พุตของเครื่องกำเนิดไฟฟ้าได้ล่วงหน้า ฉันไม่แน่ใจว่าจะต้องเสียเวลา / หน่วยความจำมากแค่ไหน