0Pricing
Pandas & NumPy Academy · บทเรียน

การหลีกเลี่ยง 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การวิเคราะห์ประสิทธิภาพด้วย timeit และ memory_profiler
  2. การหลีกเลี่ยง iterrows และลูป Python
  3. ชนิดข้อมูลที่มีประสิทธิภาพเพื่อลดการใช้หน่วยความจำ
  4. การอ่านไฟล์ขนาดใหญ่เป็นชิ้น
← กลับไปที่ Pandas & NumPy Academy