การหลีกเลี่ยง iterrows และลูป Python
แทนที่ลูปทีละแถวด้วยการดำเนินการระดับคอลัมน์แบบเวกเตอร์ np.where และ pd.cut เพื่อเพิ่มความเร็ว 10–100 เท่า
การหลีกเลี่ยง iterrows และลูป Python เป็นบทเรียน Pandas & NumPy Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Pandas & NumPy Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Pandas & NumPy Academy มีบทเรียนทั้งหมด 4 บทเรียน
ต้นทุนของการวนซ้ำทีละแถว
Pandas สร้างขึ้นบน NumPy ซึ่งประมวลผลอาร์เรย์ทั้งหมดพร้อมกันโดยใช้โค้ด C ที่คอมไพล์แล้ว เมื่อคุณวนซ้ำทีละแถวด้วย for _, row in df.iterrows() คุณจะหลีกเลี่ยงแนวทางนี้และกลับไปใช้ Python ล้วน — แต่ละแถวจะถูกดึงออกมาเป็นอ็อบเจ็กต์ Series และลูปจะทำงานในตัวแปลภาษา Python โดยมีต้นทุนประมาณ 100–1,000 เท่าของการดำเนินการแบบเวกเตอร์ที่เทียบเท่ากัน สำหรับ DataFrame ที่มี 1 ล้านแถว นี่อาจหมายถึงใช้เวลาหลายนาทีแทนที่จะเป็นมิลลิวินาที
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})
# Slow: iterrows loop
def loop_version(df):
result = []
for _, row in df.iterrows():
result.append(row['a'] + row['b'])
return pd.Series(result)
# Fast: vectorised
t_loop = timeit.timeit(lambda: loop_version(df), number=5)
t_vec = timeit.timeit(lambda: df['a'] + df['b'], number=500)
print(f'Loop (5 runs): {t_loop:.3f}s total')
print(f'Vectorised (500 runs): {t_vec:.3f}s total')
print(f'Speedup: ~{(t_loop/5)/(t_vec/500):.0f}x')แทนที่ลูปเงื่อนไขด้วย np.where
np.where(condition, value_if_true, value_if_false) เป็นรูปแบบแบบเวกเตอร์ที่เทียบเท่ากับ if/else รายสมาชิก แทนที่จะวนซ้ำแถวและเขียนคำสั่ง if ให้ส่งเงื่อนไขและค่าผลลัพธ์ทั้งสองแบบเป็นอาร์เรย์ np.where จะคำนวณสิ่งนี้ใน C สำหรับทั้งคอลัมน์พร้อมกัน ทำให้เร็วกว่าลูป Python ที่เทียบเท่ากัน 50–200 เท่าสำหรับ DataFrame ขนาดใหญ่
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'price': np.random.uniform(10, 100, 100000)})
# Slow: iterrows
def label_loop(df):
labels = []
for _, row in df.iterrows():
if row['price'] > 50:
labels.append('expensive')
else:
labels.append('cheap')
return labels
# Fast: np.where
def label_vectorised(df):
return np.where(df['price'] > 50, 'expensive', 'cheap')
# Verify equivalence on a small subset
assert list(label_loop(df.head(100))) == list(label_vectorised(df.head(100)))
print('Results match!')
print('Fast version result sample:', label_vectorised(df)[:5])จัดการหลายเงื่อนไขด้วย np.select
สำหรับเงื่อนไขตั้งแต่สามเงื่อนไขขึ้นไป ให้ใช้ np.select(condlist, choicelist, default=) แทนการซ้อน np.where ให้ส่งรายการอาร์เรย์บูลีนและรายการค่าผลลัพธ์ที่สอดคล้องกัน เงื่อนไขแรกที่ตรงจะเป็นตัวกำหนดผลลัพธ์ ส่วนแถวที่ไม่ตรงกับเงื่อนไขใดจะได้รับค่า default วิธีนี้แทนที่สาย if/elif/else ที่ซับซ้อนภายในลูปด้วยนิพจน์แบบเวกเตอร์ที่เป็นระเบียบ
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'score': np.random.randint(0, 101, 20)})
conditions = [
df['score'] >= 90,
df['score'] >= 75,
df['score'] >= 60
]
choices = ['A', 'B', 'C']
df['grade'] = np.select(conditions, choices, default='F')
print(df.sort_values('score', ascending=False).head(10))การดำเนินการกับสตริงแบบเวกเตอร์ผ่าน .str
การจัดการสตริงเป็นสาเหตุทั่วไปของลูปที่ช้า ตัวเข้าถึง .str ของ Pandas มีรูปแบบแบบเวกเตอร์ที่เทียบเท่ากับเมธอดสตริงของ Python ทั้งหมด เช่น .str.lower(), .str.strip(), .str.replace(), .str.contains() และอื่น ๆ อีกมากมาย การใช้เมธอด .str เร็วกว่าการวนซ้ำแถวและเรียกใช้เมธอดสตริงของ Python ด้วยตนเอง 10–50 เท่า
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
names = ['Alice Smith', 'BOB JONES', ' carol ', 'Dave Brown'] * 25000
df = pd.DataFrame({'name': names})
# Slow: loop
def clean_loop(df):
return [n.strip().title() for n in df['name']]
# Fast: .str accessor
def clean_vectorised(df):
return df['name'].str.strip().str.title()
t1 = timeit.timeit(lambda: clean_loop(df), number=10)
t2 = timeit.timeit(lambda: clean_vectorised(df), number=50)
print(f'Loop (10 runs): {t1:.3f}s')
print(f'.str (50 runs): {t2:.3f}s')
print(f'Speedup: {(t1/10)/(t2/50):.0f}x')
print('Sample:', clean_vectorised(df).head(4).tolist())ใช้ pd.cut และ pd.qcut แทนลูป
pd.cut() และ pd.qcut() ทำให้การแบ่งค่าเป็นช่วง ซึ่งมักเขียนเป็นลูปที่มีเงื่อนไข if/elif หลายรายการ ทำงานแบบเวกเตอร์ หากคุณพบว่าตนเองกำลังเขียน 'if value < 18: age_group = child elif value < 65: age_group = adult' ภายในลูปของแถว ให้แทนที่ด้วยการเรียก pd.cut() เพียงครั้งเดียว ซึ่งจะประมวลผลทั้งคอลัมน์พร้อมกันด้วยความเร็วระดับ C
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'age': np.random.randint(0, 90, 100000)})
# Slow: loop with conditionals
def categorise_loop(df):
result = []
for age in df['age']:
if age < 18:
result.append('child')
elif age < 65:
result.append('adult')
else:
result.append('senior')
return result
# Fast: pd.cut
def categorise_cut(df):
return pd.cut(df['age'], bins=[0, 18, 65, 100],
labels=['child', 'adult', 'senior'],
right=False)
assert list(categorise_loop(df.head(5))) == list(categorise_cut(df.head(5)).astype(str))
print('Fast version sample:', categorise_cut(df).head(5).tolist())apply() ไม่ใช่คำตอบเสมอไป
บทเรียนจำนวนมากแนะนำให้แทนที่ลูปด้วย .apply(func) แต่ภายใน apply ก็ยังเป็นลูปในระดับ Python — จึงเร็วกว่า iterrows เพียงเล็กน้อย และช้ากว่าการประมวลผลแบบเวกเตอร์จริงมาก ควรใช้ apply เฉพาะกรณีที่ไม่มีทางเลือกแบบเวกเตอร์ (เช่น ตรรกะซับซ้อนที่ใช้หลายคอลัมน์) หากฟังก์ชันในตัวของ Pandas หรือ NumPy สามารถแสดงการดำเนินการนั้นได้ ควรเลือกใช้ฟังก์ชันดังกล่าวแทน apply เสมอ
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(100000)})
# These all do the same thing — compare performance
t1 = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=20)
t2 = timeit.timeit(lambda: df['x'] ** 2, number=200)
t3 = timeit.timeit(lambda: np.square(df['x']), number=200)
print(f'apply(v**2): {t1/20*1000:.2f} ms per run')
print(f'** operator: {t2/200*1000:.2f} ms per run')
print(f'np.square(): {t3/200*1000:.2f} ms per run')แทนที่ลูป groupby ด้วย agg และ transform
รูปแบบที่พบได้ทั่วไปคือการวนซ้ำแต่ละกลุ่มด้วยตนเอง: for name, group in df.groupby('cat'): do_something(group) วิธีนี้ช้าด้วยเหตุผลเดียวกับที่ iterrows ช้า ให้แทนที่ด้วย groupby().agg() สำหรับสร้างสถิติสรุป หรือใช้ groupby().transform() เพื่อกระจายสถิติระดับกลุ่มกลับไปยังตำแหน่งแถวเดิม
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'cat': np.random.choice(['A','B','C','D'], 100000),
'val': np.random.randn(100000)
})
# Slow: manual loop to add group mean
def slow_group_mean(df):
means = {}
for name, group in df.groupby('cat'):
means[name] = group['val'].mean()
return df['cat'].map(means)
# Fast: transform
def fast_group_mean(df):
return df.groupby('cat')['val'].transform('mean')
t1 = timeit.timeit(lambda: slow_group_mean(df), number=10)
t2 = timeit.timeit(lambda: fast_group_mean(df), number=100)
print(f'Loop: {t1/10*1000:.1f} ms')
print(f'transform: {t2/100*1000:.1f} ms')
print(f'Speedup: {(t1/10)/(t2/100):.0f}x')เมื่อ iterrows ยังเหมาะสม
แม้จะช้า แต่ก็มีกรณีที่เหมาะสมสำหรับ iterrows และ itertuples: การพิมพ์หรือบันทึกข้อมูลจากแถว (ไม่ต้องคำนึงถึงประสิทธิภาพ), การสร้างข้อมูลสำหรับเรียก API ที่แต่ละแถวเป็นตัวขับเคลื่อนการเรียก HTTP (ความหน่วงของเครือข่ายเป็นปัจจัยหลัก) และ การแก้ไขข้อบกพร่อง ของแถวเฉพาะที่มีเงื่อนไขซับซ้อน หากมีแถวน้อยกว่า 1,000 แถวและการดำเนินการทำงานเพียงครั้งเดียว ความแตกต่างระหว่างลูปกับการประมวลผลแบบเวกเตอร์มีเพียงระดับมิลลิวินาที ซึ่งไม่คุ้มกับความซับซ้อนของโค้ด
import pandas as pd
import numpy as np
df = pd.DataFrame({
'order_id': [101, 102, 103],
'product': ['Widget', 'Gadget', 'Doohickey'],
'amount': [29.99, 49.99, 9.99]
})
# Acceptable use: building a structured log (small data, one-time)
for _, row in df.iterrows():
print(f'Order {row["order_id"]}: {row["product"]} — ${row["amount"]:.2f}')itertuples เร็วกว่า iterrows
หากจำเป็นต้องวนซ้ำแถวใน Python (เพราะไม่มีรูปแบบแบบเวกเตอร์ที่เทียบเท่า) ให้ใช้ itertuples() แทน iterrows() ฟังก์ชันนี้คืนค่าแต่ละแถวเป็นทูเพิลที่มีชื่อ แทนที่จะเป็น Pandas Series จึงหลีกเลี่ยงต้นทุนการสร้าง Series ทำให้โดยทั่วไปเร็วกว่า iterrows 5–10 เท่า เข้าถึงค่าด้วยรูปแบบแอตทริบิวต์: row.column_name หลีกเลี่ยงวิธีนี้หากชื่อคอลัมน์มีช่องว่าง (ไม่ใช่ชื่อแอตทริบิวต์ที่ถูกต้อง)
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})
t1 = timeit.timeit(
lambda: [row.a + row.b for row in df.itertuples()], number=10)
t2 = timeit.timeit(
lambda: [row['a'] + row['b'] for _, row in df.iterrows()], number=10)
print(f'itertuples: {t1/10*1000:.1f} ms')
print(f'iterrows: {t2/10*1000:.1f} ms')
print(f'itertuples speedup: {t2/t1:.1f}x')รายการตรวจสอบรูปแบบการประมวลผลแบบเวกเตอร์
ก่อนเขียนลูป ให้ถามคำถามต่อไปนี้:
- การดำเนินการเป็นแบบรายสมาชิกในคอลัมน์เดียวหรือไม่? → ใช้นิพจน์เลขคณิตของคอลัมน์หรือฟังก์ชัน ufunc ของ NumPy
- เกี่ยวข้องกับตรรกะแบบมีเงื่อนไขหรือไม่? → ใช้
np.where(2 เงื่อนไข) หรือnp.select(3 เงื่อนไขขึ้นไป) - ต้องแบ่งค่าเป็นช่วงหรือไม่? → ใช้
pd.cutหรือpd.qcut - ต้องดำเนินการกับสตริงหรือไม่? → ใช้ตัวเข้าถึง
.str - ต้องรวมค่าภายในกลุ่มหรือไม่? → ใช้
groupby().agg()หรือgroupby().transform()
apply() หรือ itertuples() เป็นทางเลือกสำรองเฉพาะเมื่อไม่มีข้อใดข้างต้นใช้ได้ตัวอย่างการเร่งความเร็วจริง: การคำนวณราคา
นี่คือตัวอย่างการปรับโครงสร้างโค้ดก่อนและหลังอย่างสมบูรณ์ สำหรับการคำนวณทางธุรกิจที่พบได้ทั่วไป — การใช้ส่วนลดแบบขั้นบันไดตามจำนวนสินค้าในคำสั่งซื้อ เวอร์ชันที่ใช้ลูปอ่านง่าย แต่ช้าเกินไปสำหรับ DataFrame ขนาดใหญ่ ส่วนเวอร์ชันแบบเวกเตอร์ที่ใช้ np.select ให้ผลลัพธ์เดียวกันโดยใช้เวลาเพียงหนึ่งในสิบ
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'price': np.random.uniform(10, 200, 100000),
'qty': np.random.randint(1, 500, 100000)
})
# BEFORE: loop with conditionals
def slow_discount(df):
result = []
for _, row in df.iterrows():
if row['qty'] >= 100:
disc = 0.2
elif row['qty'] >= 50:
disc = 0.1
elif row['qty'] >= 10:
disc = 0.05
else:
disc = 0.0
result.append(row['price'] * (1 - disc))
return pd.Series(result)
# AFTER: np.select
def fast_discount(df):
conditions = [df['qty'] >= 100, df['qty'] >= 50, df['qty'] >= 10]
discounts = [0.20, 0.10, 0.05]
disc = np.select(conditions, discounts, default=0.0)
return df['price'] * (1 - disc)
assert slow_discount(df.head(200)).round(4).equals(fast_discount(df.head(200)).reset_index(drop=True).round(4))
print('Both versions agree!')ตรวจสอบความเข้าใจอย่างรวดเร็ว
ทดสอบความเข้าใจเกี่ยวกับการประมวลผลแบบเวกเตอร์จากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า iterrows ช้ากว่าการดำเนินการแบบเวกเตอร์ 100–1,000 เท่า np.where และ np.select ใช้แทนลูปที่มีเงื่อนไข ตัวเข้าถึง .str ทำให้การดำเนินการกับสตริงเป็นแบบเวกเตอร์ และ groupby().transform() ใช้แทนการวนซ้ำกลุ่มด้วยตนเอง เมื่อจำเป็นต้องวนซ้ำ ให้ใช้ itertuples แทน iterrows เพื่อเพิ่มประสิทธิภาพ 5–10 เท่า บทถัดไปเราจะสำรวจชนิดข้อมูลที่มีประสิทธิภาพ — การลดขนาดชนิดข้อมูลตัวเลขและการใช้ Categorical เพื่อลดการใช้หน่วยความจำได้สูงสุด 70%
คำถามที่พบบ่อย
บทเรียน “การหลีกเลี่ยง iterrows และลูป Python” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การหลีกเลี่ยง iterrows และลูป Python” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Pandas & NumPy Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Pandas & NumPy Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การหลีกเลี่ยง iterrows และลูป Python”
แทนที่ลูปทีละแถวด้วยการดำเนินการระดับคอลัมน์แบบเวกเตอร์ np.where และ pd.cut เพื่อเพิ่มความเร็ว 10–100 เท่า คุณปฏิบัติ Pandas & NumPy Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Pandas & NumPy Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Pandas & NumPy Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “การหลีกเลี่ยง iterrows และลูป Python” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Pandas & NumPy Academy นี้ได้ไหม
ได้ บทเรียน Pandas & NumPy Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การวิเคราะห์ประสิทธิภาพด้วย timeit และ memory_profiler
- การหลีกเลี่ยง iterrows และลูป Python
- ชนิดข้อมูลที่มีประสิทธิภาพเพื่อลดการใช้หน่วยความจำ
- การอ่านไฟล์ขนาดใหญ่เป็นชิ้น