Machine Learning › Feature engineering

Encoding ข้อมูลหมวดหมู่

ประมาณ 30 นาที

เป้าหมายของบท

  • แปลงข้อมูลหมวดหมู่เป็นตัวเลขด้วย one-hot และเลือกใช้ ordinal เมื่อมีลำดับจริงเท่านั้น
  • ใช้ OneHotEncoder ที่ fit บน train เพื่อให้คอลัมน์ตรงกันและรับหมวดใหม่ได้
  • รู้ว่า target encoding ต้องคำนวณจาก train เท่านั้น

โมเดลกินได้แต่ตัวเลข

คอลัมน์ข้อความอย่าง city, channel, first_payment_method ต้องแปลงเป็นตัวเลขก่อนเข้าโมเดล วิธีที่ใช้บ่อยที่สุดคือ one-hot encoding: สร้างคอลัมน์ 0/1 หนึ่งคอลัมน์ต่อหนึ่งหมวด

pd.get_dummies(df['city'])                    # 8 เมือง → 8 คอลัมน์
pd.get_dummies(df['city'], drop_first=True)   # ทิ้งหมวดแรกไป → 7 คอลัมน์

drop_first=True ตัดคอลัมน์ที่คำนวณได้จากคอลัมน์อื่น (ถ้า 7 เมืองเป็น 0 หมดก็ต้องเป็นเมืองที่ 8) ช่วยลดความซ้ำซ้อนโดยเฉพาะกับโมเดลเส้นตรง

get_dummies vs OneHotEncoder

pd.get_dummies สะดวกสำหรับสำรวจ แต่ จำหมวดจากข้อมูลที่ส่งเข้าไปเท่านั้น — ถ้าแปลง train กับ test แยกกัน คอลัมน์อาจไม่ตรงกัน (เช่น เมืองที่ไม่มีใน test) OneHotEncoder ของ scikit-learn ต้อง .fit บนข้อมูล train แล้ว .transform ทั้ง train และ test จึงได้คอลัมน์ชุดเดียวกันเสมอ และตั้ง handle_unknown='ignore' เพื่อให้หมวดใหม่ที่ไม่เคยเห็นตอนเทรนกลายเป็นแถวที่เป็น 0 ทั้งหมด แทนที่จะ error

from sklearn.preprocessing import OneHotEncoder

encoder = OneHotEncoder(handle_unknown='ignore').fit(train[['city']])
encoder.transform(new_rows[['city']])

ordinal encoding: ระวังลำดับปลอม

OrdinalEncoder แปลงหมวดเป็น 0, 1, 2, … เหมาะกับข้อมูลที่ มีลำดับจริง (เช่น น้อย/กลาง/มาก) ถ้าใช้กับข้อมูลที่ไม่มีลำดับ (เช่น วิธีชำระเงิน) โมเดลเส้นตรงจะเข้าใจว่า promptpay (3) “มากกว่า” cod (1) ซึ่งไม่มีความหมาย — ใช้ one-hot แทน

target encoding: ระวัง leakage

การแทนหมวดด้วยค่าเฉลี่ยของ target ในหมวดนั้น (เช่น อัตราซื้อซ้ำเฉลี่ยของแต่ละช่องทาง) อาจมีประโยชน์ แต่ ต้องคำนวณจากชุด train เท่านั้น ถ้าคำนวณจากข้อมูลทั้งชุด ค่าเฉลี่ยจะมี target ของแถว test ปนอยู่ — เป็น leakage ที่ทำให้ผลทดสอบดีเกินจริง

ตัวอย่างแก้โจทย์

กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้

one-hot ด้วย get_dummies

แปลงคอลัมน์เมืองให้โมเดลใช้ได้

  1. เมืองมี 8 หมวด → get_dummies ได้ 8 คอลัมน์

    Python

OneHotEncoder กับหมวดที่ไม่เคยเห็น

ลูกค้าจากเมืองที่ไม่มีในข้อมูลเทรนจะเป็นอย่างไร

  1. fit encoder บนข้อมูลที่ไม่มี Hat Yai — ได้ 7 หมวด

    Python

เติมช่องว่าง

แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ

ยังไม่ได้ทำ

fit OneHotEncoder บนข้อมูล train แล้วนับจำนวนหมวดที่รู้จัก เก็บไว้ในตัวแปร n_cats เติม ___ ให้ครบ

แบบฝึกท้ายบท

บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม

🟢 พื้นฐาน

🟢 พื้นฐานยังไม่ได้ทำ

คอลัมน์ city มีกี่หมวด เก็บไว้ในตัวแปร n_cities

🟢 พื้นฐานยังไม่ได้ทำ

pd.get_dummies(df["city"]) ได้กี่คอลัมน์ เก็บไว้ในตัวแปร n_dummy_cols

🟢 พื้นฐานยังไม่ได้ทำ

ถ้าใส่ drop_first=True จะได้กี่คอลัมน์ เก็บไว้ในตัวแปร n_dummy_drop

🟢 พื้นฐานยังไม่ได้ทำ

เมืองที่มีลูกค้ามากที่สุด (ชื่อเมืองเป็น string) เก็บไว้ในตัวแปร top_city

🟡 ประยุกต์

🟡 ประยุกต์ยังไม่ได้ทำ

fit OneHotEncoder(handle_unknown="ignore") บนลูกค้าที่ไม่ใช่ Hat Yai แล้ว transform ลูกค้า Hat Yai: เก็บจำนวนลูกค้า Hat Yai และผลรวมของค่าทั้งหมดหลัง transform ไว้ในตัวแปร n_hatyai, unseen_sum

🟡 ประยุกต์ยังไม่ได้ทำ

แปลง train (ไม่มี Hat Yai) กับชุดที่มีแต่ Hat Yai ด้วย pd.get_dummies แยกกัน แล้วเก็บจำนวนคอลัมน์ของแต่ละชุดไว้ในตัวแปร train_cols, hatyai_cols

🟡 ประยุกต์ยังไม่ได้ทำ

หมวดทั้งหมดของ first_payment_method ตามที่ OrdinalEncoder รู้จัก (เรียงตามตัวอักษร เก็บเป็น list) เก็บไว้ในตัวแปร payment_cats

🔴 ท้าทาย

🔴 ท้าทายยังไม่ได้ทำ

target encoding แบบถูกวิธี: อัตราซื้อซ้ำเฉลี่ยของช่องทาง promo คำนวณจากชุด train เท่านั้น ปัด 4 ตำแหน่ง เก็บไว้ในตัวแปร te_train_promo

🔴 ท้าทายยังไม่ได้ทำ

ถ้าคำนวณอัตราซื้อซ้ำของ promo จากข้อมูลทั้งชุด (รวมแถว test) แล้วลบด้วยค่าจากชุด train เพียงอย่างเดียว ผลต่างเท่าไร (ปัด 4 ตำแหน่ง) — นี่คือส่วนที่ target ของแถว test รั่วเข้ามา เก็บไว้ในตัวแปร te_gap

Quiz

1. ทำไมต้องแปลงคอลัมน์หมวดหมู่เป็นตัวเลขก่อนเข้าโมเดลส่วนใหญ่ใน scikit-learn
2. drop_first=True ใน get_dummies มีไว้ทำไม
3. handle_unknown="ignore" ของ OneHotEncoder ทำอะไร
4. ทำไมไม่ควรใช้ OrdinalEncoder กับวิธีชำระเงิน (cod, promptpay, …)

สรุป

  • one-hot: หนึ่งหมวดต่อหนึ่งคอลัมน์ 0/1 (drop_first ตัดคอลัมน์ซ้ำซ้อน) — OrdinalEncoder ใช้เฉพาะข้อมูลที่มีลำดับจริง
  • fit encoder บน train แล้ว transform ทั้ง train/test และตั้ง handle_unknown="ignore" เพื่อรับหมวดใหม่โดยไม่ error
  • target encoding ต้องคำนวณจาก train เท่านั้น — ถ้าใช้ข้อมูลทั้งชุด ค่าเฉลี่ยของ promo เพี้ยนไปจากค่าที่ควรเป็น (ตัวอย่างในบทต่างกัน ~0.004) เพราะ target ของ test รั่วเข้ามา

คำถามสัมภาษณ์

one-hot กับ ordinal encoding ต่างกันยังไง และเลือกใช้เมื่อไร

one-hot สร้างคอลัมน์ 0/1 ต่อหนึ่งหมวด ไม่สร้างลำดับ จึงเหมาะกับข้อมูลที่ไม่มีลำดับ (เมือง ช่องทาง วิธีชำระเงิน) ส่วน ordinal แปลงเป็นเลข 0,1,2 ตามลำดับหมวด เหมาะเมื่อหมวดมีลำดับจริงและระยะห่างพอสมเหตุสมผล (เช่น ระดับความพึงพอใจ) ถ้าใช้ ordinal กับข้อมูลที่ไม่มีลำดับ โมเดลเส้นตรงอาจตีความเป็นมาก-น้อยที่ไม่มีอยู่จริง

เจอคอลัมน์ที่มีหมวดเป็นพันๆ ค่า (high cardinality) เช่น รหัสสินค้า จะจัดการอย่างไร

one-hot ตรงๆ จะสร้างคอลัมน์มหาศาลและกระจัดกระจาย ทางเลือกได้แก่ รวมหมวดที่พบน้อยเป็น "อื่นๆ", ใช้ target encoding (คำนวณจาก train เท่านั้น หรือทำแบบ cross-fitted เพื่อกัน leakage), ใช้ embedding หรือโมเดลที่รับหมวดได้โดยตรง และเลือกวิธีด้วย cross-validation