apply
ฟังก์ชันอำนวยความสะดวกที่คุณไม่เคยต้องการ
เราเริ่มต้นด้วยการตอบคำถามใน OP ทีละคำถาม
" ถ้าสมัครไม่ดีทำไมถึงอยู่ใน API "
DataFrame.apply
และSeries.apply
เป็นฟังก์ชันอำนวยความสะดวกที่กำหนดบนวัตถุ DataFrame และ Series ตามลำดับ apply
ยอมรับฟังก์ชันที่ผู้ใช้กำหนดซึ่งใช้การแปลง / การรวมบน DataFrame apply
เป็นกระสุนเงินอย่างมีประสิทธิภาพที่ทำสิ่งที่ฟังก์ชันแพนด้าที่มีอยู่ไม่สามารถทำได้
บางสิ่งapply
สามารถทำได้:
- เรียกใช้ฟังก์ชันที่ผู้ใช้กำหนดเองบน DataFrame หรือ Series
- ใช้ฟังก์ชัน row-wise (
axis=1
) หรือ column-wise ( axis=0
) บน DataFrame
- ดำเนินการจัดตำแหน่งดัชนีในขณะที่ใช้ฟังก์ชัน
- ทำการรวมกับฟังก์ชันที่ผู้ใช้กำหนดเอง (อย่างไรก็ตามเรามักจะชอบ
agg
หรือtransform
ในกรณีเหล่านี้)
- ทำการแปลงองค์ประกอบอย่างชาญฉลาด
- ถ่ายทอดผลลัพธ์รวมไปยังแถวเดิม (ดู
result_type
อาร์กิวเมนต์)
- ยอมรับอาร์กิวเมนต์ตำแหน่ง / คีย์เวิร์ดเพื่อส่งผ่านไปยังฟังก์ชันที่ผู้ใช้กำหนดเอง
...ท่ามกลางคนอื่น ๆ. สำหรับข้อมูลเพิ่มเติมโปรดดูแอปพลิเคชันฟังก์ชันแถวหรือคอลัมน์ในเอกสารประกอบ
ดังนั้นด้วยคุณสมบัติทั้งหมดนี้ทำไมถึงapply
ไม่ดี? มันเป็นเพราะapply
เป็น ช้า นุ่นไม่ได้ตั้งสมมติฐานเกี่ยวกับลักษณะของฟังก์ชันของคุณดังนั้นจึงใช้ฟังก์ชันของคุณซ้ำ ๆ กับแต่ละแถว / คอลัมน์ตามความจำเป็น นอกจากนี้การจัดการทั้งหมดของสถานการณ์ข้างต้นหมายถึงการapply
เกิดขึ้นบางส่วนค่าใช้จ่ายที่สำคัญในแต่ละซ้ำ นอกจากนี้ยังapply
ใช้หน่วยความจำมากขึ้นซึ่งเป็นความท้าทายสำหรับแอปพลิเคชันที่มีขอบเขตหน่วยความจำ
มีสถานการณ์น้อยมากที่apply
เหมาะสมที่จะใช้ (ดูข้อมูลเพิ่มเติมด้านล่าง) หากคุณไม่แน่ใจว่าควรใช้apply
หรือไม่คุณอาจไม่ควรใช้
มาตอบคำถามต่อไป
" ฉันควรทำให้รหัสของฉันใช้ฟรีได้อย่างไรและเมื่อใด "
ใช้ถ้อยคำที่นี่มีบางสถานการณ์ทั่วไปที่คุณจะต้องได้รับการกำจัดของสายใด ๆ apply
ที่จะ
ข้อมูลตัวเลข
หากคุณกำลังทำงานกับข้อมูลตัวเลขเป็นไปได้ว่ามีฟังก์ชัน cython แบบเวกเตอร์ที่ทำสิ่งที่คุณพยายามทำอยู่แล้ว (หากไม่เป็นเช่นนั้นโปรดถามคำถามใน Stack Overflow หรือเปิดคำขอคุณลักษณะบน GitHub)
เปรียบเทียบประสิทธิภาพของapply
การใช้งานเพิ่มเติมอย่างง่าย
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
ประสิทธิภาพที่ชาญฉลาดไม่มีการเปรียบเทียบการเทียบเท่าของ cythonized นั้นเร็วกว่ามาก ไม่จำเป็นต้องใช้กราฟเพราะความแตกต่างนั้นชัดเจนแม้กระทั่งข้อมูลของเล่น
%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)
แม้ว่าคุณจะเปิดใช้งานการส่งผ่านอาร์เรย์ดิบด้วยraw
อาร์กิวเมนต์ แต่ก็ยังช้าเป็นสองเท่า
%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
ตัวอย่างอื่น:
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)
โดยทั่วไปให้ค้นหาทางเลือกที่เป็นเวกเตอร์หากเป็นไปได้
สตริง / Regex
Pandas มีฟังก์ชันสตริง "vectorized" ในสถานการณ์ส่วนใหญ่ แต่มีบางกรณีที่ไม่เกิดขึ้นบ่อยนักที่ฟังก์ชันเหล่านั้นไม่ ... "ใช้" เพื่อที่จะพูด
ปัญหาทั่วไปคือการตรวจสอบว่ามีค่าในคอลัมน์อยู่ในคอลัมน์อื่นของแถวเดียวกันหรือไม่
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
สิ่งนี้ควรส่งคืนแถวที่สองและแถวที่สามเนื่องจาก "donald" และ "minnie" อยู่ในคอลัมน์ "Title" ตามลำดับ
โดยใช้ Apply สิ่งนี้จะทำได้โดยใช้
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
อย่างไรก็ตามมีทางออกที่ดีกว่าโดยใช้การทำความเข้าใจรายการ
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)
สิ่งที่ควรทราบก็คือกิจวัตรซ้ำ ๆ เกิดขึ้นเร็วกว่าapply
เนื่องจากค่าใช้จ่ายที่ต่ำกว่า หากคุณต้องการจัดการ NaN และ dtypes ที่ไม่ถูกต้องคุณสามารถสร้างสิ่งนี้โดยใช้ฟังก์ชันที่กำหนดเองจากนั้นคุณสามารถเรียกด้วยอาร์กิวเมนต์ภายในความเข้าใจรายการ
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับเวลาที่ควรพิจารณาความเข้าใจในรายการเป็นตัวเลือกที่ดีโปรดดูบทความของฉัน: สำหรับลูปที่มีหมีแพนด้า - ฉันควรดูแลเมื่อใด .
หมายเหตุ
การดำเนินการวันที่และวันที่และเวลายังมีเวอร์ชัน vectorized ดังนั้นสำหรับตัวอย่างเช่นคุณจะชอบpd.to_datetime(df['date'])
มากกว่า, df['date'].apply(pd.to_datetime)
กล่าวว่า
อ่านเพิ่มเติมได้ที่
เอกสาร
ข้อผิดพลาดทั่วไป: การระเบิดคอลัมน์ของรายการ
s = pd.Series([[1, 2]] * 3)
s
0 [1, 2]
1 [1, 2]
2 [1, 2]
dtype: object
apply(pd.Series)
คนอยากจะใช้ นี่เป็นเรื่องที่น่ากลัวในแง่ของประสิทธิภาพ
s.apply(pd.Series)
0 1
0 1 2
1 1 2
2 1 2
ตัวเลือกที่ดีกว่าคือแสดงรายการคอลัมน์และส่งต่อไปยัง 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)
สุดท้ายนี้
" มีสถานการณ์ไหน apply
บ้างที่ดี "
สมัครเป็นฟังก์ชั่นอำนวยความสะดวกเพื่อให้มีเป็นสถานการณ์ที่ค่าใช้จ่ายก็พอเพียงเล็กน้อยที่จะให้อภัย จริงๆมันขึ้นอยู่กับจำนวนครั้งที่เรียกใช้ฟังก์ชัน
ฟังก์ชันที่เป็น Vectorized สำหรับ Series แต่ไม่ใช่ DataFrames
จะเกิดอะไรขึ้นถ้าคุณต้องการใช้การดำเนินการแบบสตริงกับหลายคอลัมน์ จะเกิดอะไรขึ้นถ้าคุณต้องการแปลงหลายคอลัมน์เป็นวันที่และเวลา? ฟังก์ชันเหล่านี้เป็นแบบเวกเตอร์สำหรับซีรี่ส์เท่านั้นดังนั้นจึงต้องใช้กับแต่ละคอลัมน์ที่คุณต้องการแปลง / ดำเนินการ
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
นี่เป็นกรณีที่ยอมรับได้สำหรับapply
:
df.apply(pd.to_datetime, errors='coerce').dtypes
date1 datetime64[ns]
date2 datetime64[ns]
dtype: object
โปรดทราบว่ามันก็สมเหตุสมผลstack
หรือแค่ใช้การวนซ้ำที่ชัดเจน ตัวเลือกทั้งหมดนี้เร็วกว่าการใช้เล็กน้อยapply
แต่ความแตกต่างนั้นเล็กน้อยพอที่จะให้อภัยได้
%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)
คุณสามารถสร้างกรณีที่คล้ายกันสำหรับการดำเนินการอื่น ๆ เช่นการดำเนินการสตริงหรือการแปลงเป็นหมวดหมู่
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)
และอื่น ๆ ...
การแปลงซีรี่ส์เป็นstr
: astype
กับapply
ดูเหมือนจะเป็นความแปลกประหลาดของ API โดยใช้apply
การแปลงจำนวนเต็มในชุดสตริงก็เปรียบได้ (และบางครั้งเร็วกว่า) astype
กว่าการใช้
กราฟถูกพล็อตโดยใช้perfplot
ไลบรารี
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())
ด้วยลอยฉันเห็นเป็นอย่างสม่ำเสมอให้เร็วที่สุดเท่าหรือเร็วกว่าเล็กน้อยastype
apply
สิ่งนี้เกี่ยวข้องกับข้อเท็จจริงที่ว่าข้อมูลในการทดสอบเป็นประเภทจำนวนเต็ม
GroupBy
การดำเนินการกับการเปลี่ยนแปลงที่ถูกล่ามโซ่
GroupBy.apply
ยังไม่ได้รับการกล่าวถึงจนถึงขณะนี้ แต่GroupBy.apply
ยังเป็นฟังก์ชันอำนวยความสะดวกแบบวนซ้ำเพื่อจัดการกับสิ่งที่GroupBy
ฟังก์ชันที่มีอยู่ไม่มี
ข้อกำหนดทั่วไปประการหนึ่งคือการดำเนินการ GroupBy จากนั้นการดำเนินการหลักสองอย่างเช่น "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
คุณต้องมีการโทรแบบกลุ่มต่อเนื่องสองครั้งที่นี่:
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
โดยใช้apply
คุณสามารถย่อให้สั้นลงเป็นการโทรครั้งเดียว
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
เป็นการยากมากที่จะหาปริมาณประสิทธิภาพเนื่องจากขึ้นอยู่กับข้อมูล แต่โดยทั่วไปapply
เป็นวิธีแก้ปัญหาที่ยอมรับได้หากเป้าหมายคือการลดการgroupby
โทร (เนื่องจากgroupby
มีราคาค่อนข้างแพงด้วย)
ข้อควรระวังอื่น ๆ
นอกเหนือจากคำเตือนที่กล่าวถึงข้างต้นแล้วยังควรกล่าวถึงด้วยว่าapply
ทำงานในแถวแรก (หรือคอลัมน์) สองครั้ง ทำเพื่อตรวจสอบว่าฟังก์ชันนี้มีผลข้างเคียงหรือไม่ หากไม่เป็นเช่นapply
นั้นอาจสามารถใช้เส้นทางลัดในการประเมินผลลัพธ์ได้มิฉะนั้นจะกลับไปใช้งานช้า
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
พฤติกรรมนี้ยังพบได้ในGroupBy.apply
เวอร์ชันแพนด้า <0.25 (แก้ไขที่ 0.25 ดูข้อมูลเพิ่มเติมได้ที่นี่)
returns.add(1).apply(np.log)
เทียบกับnp.log(returns.add(1)
เป็นกรณีที่apply
โดยทั่วไปจะเร็วขึ้นเล็กน้อยซึ่งเป็นกล่องสีเขียวล่างขวาในแผนภาพของ jpp ด้านล่าง