สัปดาห์ที่ 10 · Data Project · End-to-end

Data Project ปลายทาง

ปิดเฟส Data ด้วยการ "เล่าเรื่องด้วยข้อมูล" · เก็บข้อมูลจริง → ทำความสะอาด → วิเคราะห์ → ทำกราฟ → เขียน insight 1 หน้า · จบขั้นนี้ = ทำ data report ได้เอง

เป้าหมายสัปดาห์นี้

🔒 W10 มี 2 อย่างที่ AI ทำให้คุณไม่ได้
  1. ตัดสินใจเรื่อง privacy — AI ไม่รู้ว่าเพื่อนคุณยินยอมให้ใช้ข้อมูลมั้ย · ไม่รู้ว่า GPA / สุขภาพ / เงิน ของใคร · ไม่รู้ว่า raw data จะไป public หรือเปล่า
  2. ตรวจว่า analysis ถูก — AI hallucinate ตัวเลขได้ · เสนอ correlation ที่ไม่มี · drop row ที่ไม่ควร drop · สรุปเกินที่ data รองรับ · คุณคือคนที่ลงชื่อใน report

🔐 Privacy & Security — ก่อนเก็บข้อมูลคนอื่น

ถ้าโจทย์ของคุณคือ "ขอให้เพื่อน 30 คนกรอก Google Form" — คุณกำลังเป็น "data controller" ตามกฎหมาย PDPA (พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล 2562) · ทำผิดมีโทษได้ · ทำถูกใช้งานได้ทั้งชีวิต

✅ 6-Question Consent Checklist

ก่อนส่ง form ให้เพื่อน ตอบ checklist นี้ทั้ง 6 ข้อให้ครบ:

คำถามต้องตอบยังไง
1. คนตอบรู้มั้ย ว่าฉันเอาข้อมูลไปทำอะไร? ใส่ใน description ของ Form: "ใช้สำหรับวิชา CP เท่านั้น · จะรายงานเฉพาะค่าเฉลี่ย ไม่ระบุชื่อ"
2. ให้ตัวเลือก "ไม่ตอบ" มั้ย? Form ทุกคำถามต้อง ไม่บังคับ (uncheck "Required") ยกเว้น consent
3. ขอ consent ชัด ๆ มั้ย? คำถามแรก: "ฉันยินยอมให้ใช้ข้อมูลนี้สำหรับงานในคอร์ส" ☐ ยินยอม / ☐ ไม่ยินยอม
4. ขอข้อมูลที่ ระบุตัวตน มั้ย? ลบ Email collection ใน Form settings · ห้ามถามชื่อจริง · student_id ถ้าไม่จำเป็น ห้ามถาม
5. ใครเห็น raw data ได้บ้าง? แค่คุณ + อาจารย์ · ห้าม commit raw CSV ขึ้น GitHub public
6. หลังคอร์สจบ ลบมั้ย? ใส่ใน Form: "ข้อมูลจะถูกลบหลังเทอมจบ" · แล้วลบจริง ๆ

🏷 ระดับความเสี่ยงของข้อมูล (Sensitivity Tiers)

Tierประเภทตัวอย่างวิธีจัดการ
🟢 Tier 1: Safe Public / Aggregate PM2.5, สภาพอากาศ, public dataset, lab equipment usage ใช้ได้เลย · public ได้ทุกระดับ
🟡 Tier 2: Care Personal Habits นอน, อาหาร, AI usage, time tracking ของ user แต่ละคน ต้อง consent + anonymize · raw data ห้าม public
🔴 Tier 3: Sensitive GPA / Health / Finance / Opinions คะแนน, สุขภาพ, รายได้, รายจ่าย, ความคิดเห็นแอบ หลีกเลี่ยงถ้าทำได้ · ถ้าจำเป็น → consent + ลบ identifier + รายงานเฉพาะ aggregate
⛔ ห้ามทำ ข้อมูลที่ระบุ + เปิดเผยได้ คะแนนของเพื่อนที่ระบุชื่อ · รูปคนอื่นที่ไม่ขอ · social media posts ที่ไม่ใช่ของตัวเอง ไม่ทำ · ใช้ public alternative

🛡 Anonymization — ลบ identifier ก่อน analyze

import pandas as pd import hashlib # raw data from Google Forms — มี email + name (PII) raw = pd.DataFrame([ {"email": "ploy@ubu.ac.th", "name": "Ploy K.", "sleep_hr": 7, "gpa": 3.45}, {"email": "bank@ubu.ac.th", "name": "Bank S.", "sleep_hr": 5, "gpa": 3.20}, {"email": "mint@ubu.ac.th", "name": "Mint P.", "sleep_hr": 8, "gpa": 3.78}, {"email": "tum@ubu.ac.th", "name": "Tum L.", "sleep_hr": 6, "gpa": 2.90}, ]) print("=== RAW (มี PII) — ห้าม commit ขึ้น GitHub ===") print(raw) # Anonymize — แทน email + name ด้วย hash def hash_id(s): return "U" + hashlib.sha256(s.encode()).hexdigest()[:6] anon = raw.copy() anon["user_id"] = anon["email"].apply(hash_id) anon = anon.drop(columns=["email", "name"]) # Re-order anon = anon[["user_id", "sleep_hr", "gpa"]] print("\n=== ANONYMIZED — commit ตัวนี้ได้ ===") print(anon) # เซฟทั้ง 2 — raw ใน /private (gitignore), anon ใน /data # (ใน Cursor: ใส่ "private/" ใน .gitignore) raw.to_csv("private_raw.csv", index=False, encoding="utf-8") anon.to_csv("data_anon.csv", index=False, encoding="utf-8") print("\n✅ raw → private/ (gitignored) · anon → data/ (commit ได้)")

📁 Folder structure ที่ปลอดภัย

my-data-project/
├── .gitignore                    ← สำคัญที่สุด
├── private/                      ← ใน .gitignore
│   └── raw_responses.csv         ← มี email + name
├── data/                         ← commit ได้
│   ├── responses_anon.csv        ← anonymized
│   └── responses_clean.csv
├── notebook.ipynb
├── README.md
└── .env                          ← ใน .gitignore (ถ้ามี API key)

.gitignore ต้องมี:

private/
*.raw.csv
.env
.env.local
⚠️ ครั้งหนึ่งใน git history = ตลอดกาล ถ้า commit raw CSV ครั้งเดียวแล้วลบทีหลัง — ข้อมูลยังอยู่ใน git history · ต้อง git filter-branch หรือ BFG Repo-Cleaner ลบจริง ๆ · ทางที่ดีที่สุดคือ "ไม่ commit ตั้งแต่แรก"

📋 Project Spec — Mini Data Story

แต่ละนักศึกษาเลือก 1 หัวข้อ · ทำ end-to-end ใน 1 สัปดาห์ · เลือกตาม Tier

หัวข้อที่แนะนำ (จัดตาม Tier ความเสี่ยง)

Tierหัวข้อวิธีได้ข้อมูลคำถามตัวอย่าง
🟢 1 มลพิษ PM2.5 / อากาศ IQAir API หรือ aqicn.org วันไหน/ช่วงไหนค่าสูงสุด? เปลี่ยนตามฤดูยังไง?
🟢 1 การใช้ห้อง lab / เครื่องมือ booking log ของห้องภาควิชา (anonymous) เครื่องไหนใช้บ่อย? วันไหน peak?
🟢 1 การเดินทาง / TomTom data.go.th / TomTom Traffic API เวลาไหนช้าที่สุด?
🟢 1 Public dataset Kaggle · data.go.th วิเคราะห์ trend
🟡 2 นิสัยการนอน / กิจวัตร Form 30+ คน · consent + anon กิจกรรมไหนกินเวลามากที่สุด?
🟡 2 การใช้ AI ของนักศึกษา Form 30+ คน · consent + anon ใช้ AI กับงานไหนบ่อยที่สุด?
🟡 2 ค่าใช้จ่ายของตัวเอง (1 คน) บันทึกของตัวเอง 30 วัน — ไม่ขอจากเพื่อน หมวดไหนใช้เยอะ?
🔴 3 GPA / ผลคะแนน หลีกเลี่ยง — ถ้าจำเป็น ใช้ของตัวเองเทียบกับ public average เท่านั้น
เคล็ดลับ — เริ่ม Tier 1 ก่อน 90% ของ data project นักศึกษาปี 1 ไม่ต้องเก็บ personal data · public dataset เพียงพอแล้วสำหรับฝึก pandas/groupby/plot
หลักคิด — ข้อมูลเล็กก็เล่าเรื่องได้ 30-50 records ก็เริ่มเห็น pattern แล้ว · อย่าตั้งเป้าหา big data — focus ที่ "คำถาม"

🔄 Pipeline 5 ขั้น

flowchart LR collect[/"1. Collect
Forms / API / CSV"/] clean["2. Clean
fix missing/wrong"] analyze["3. Analyze
groupby · calc · stats"] viz[("4. Visualize
4-5 กราฟ")] story(["5. Tell Story
insight 1 หน้า"]) collect --> clean --> analyze --> viz --> story classDef io fill:#0d3f3a,stroke:#2dd4bf,color:#fff classDef wrangle fill:#5c2618,stroke:#ffd43b,color:#fff classDef compute fill:#3b2962,stroke:#8b5cf6,color:#fff classDef plot fill:#1e3a5f,stroke:#3776ab,color:#fff classDef final fill:#064e3b,stroke:#34d399,stroke-width:2px,color:#fff class collect io class clean wrangle class analyze compute class viz plot class story final

ทุกขั้นเชื่อมกัน — ถ้าขั้นใดทำผิด ผลลัพธ์ขั้นถัด ๆ ไปจะ "แตกตาม"

1️⃣ Collect — เก็บข้อมูล

วิธี A: Google Forms

  1. สร้าง Form ที่ forms.google.com
  2. ออกแบบคำถาม "ที่ตอบง่าย / ตอบเหมือนกันได้" — multiple choice ดีกว่า open-ended
  3. กระจายให้เพื่อน → รอ 30+ responses
  4. Form → ResponsesDownload as CSV

วิธี B: Public dataset

วิธี C: API call (preview ของ W12)

import requests
import pandas as pd

# IQAir example (PM2.5)
url = "https://api.airvisual.com/v2/city?city=Ubon Ratchathani&state=Ubon Ratchathani&country=Thailand&key=YOUR_KEY"
r = requests.get(url)
data = r.json()

2️⃣ Clean — ทำความสะอาด

ข้อมูลจริง "สกปรกเสมอ" — missing, typo, duplicate · ต้องจัดการก่อน แต่ระวัง AI ที่ลบของไม่ควรลบ

import pandas as pd # Sample dirty data — เลียนแบบ Google Form responses data = { "id": ["U1", "U2", "U3", "U4", "U5", "U6", "U7", "U7", "U8"], # U7 ซ้ำ "sleep_hr": ["7", "5", "8", " 6 ", "abc", None, "7.5", "7.5", "9"], # type มั่ว "gpa": [3.45, 3.20, 3.78, 2.90, 3.10, 3.55, None, None, 3.80], # null "major": ["MAE", "ee", " MAE ", "CHE", "MAE", "Mae", "EE", "EE", "MAE"], # case/space } df = pd.DataFrame(data) print("=== ก่อน clean ===") print(df) print(f"shape: {df.shape}") # Step 1: duplicate df = df.drop_duplicates(subset=["id"]) print(f"\nหลัง drop_duplicates: {df.shape}") # Step 2: type — แปลง sleep_hr เป็นเลข (typo "abc" → NaN) df["sleep_hr"] = pd.to_numeric(df["sleep_hr"], errors="coerce") # Step 3: clean string (major) — strip + upper df["major"] = df["major"].str.strip().str.upper() # Step 4: missing values — handle ทีละ column df["sleep_hr"] = df["sleep_hr"].fillna(df["sleep_hr"].median()) # ใส่ median # gpa: เก็บ null ไว้ก่อน (ดูใน analyze) print("\n=== หลัง clean ===") print(df) # Step 5: add derived column df["sleep_category"] = pd.cut( df["sleep_hr"], bins=[0, 6, 8, 24], labels=["น้อย", "พอดี", "เยอะ"] ) print("\n=== พร้อม sleep_category ===") print(df[["id", "sleep_hr", "sleep_category"]])
⚠️ AI ชอบใช้ df.dropna() ทันที — ระวัง! dropna() "ไม่มี argument" = ลบทุกแถวที่มี null แม้แต่ column เดียว · ถ้าเรา gpa null 30% และ sleep_hr null 10% — อาจ ลบไป 40% ของข้อมูล · ใช้ subset=["col1"] เสมอ — ระบุชัดว่าจะลบเมื่อ column ไหนเป็น null
หลักการ data cleaning — เก็บ "raw" file ไว้เสมอ · clean ลงเป็นไฟล์ใหม่ · ใน notebook บันทึกเหตุผลว่า "ทำไมลบ row นี้"

3️⃣ Analyze — ตอบคำถาม

import pandas as pd import numpy as np # ใช้ข้อมูลจริงจาก step Clean np.random.seed(42) df = pd.DataFrame({ "id": [f"U{i}" for i in range(40)], "sleep_hr": np.random.normal(7, 1.5, 40).round(1), "gpa": np.random.normal(3.2, 0.4, 40).round(2), "major": np.random.choice(["MAE", "EE", "ChE"], 40), }) df["sleep_category"] = pd.cut(df["sleep_hr"], bins=[0, 6, 8, 24], labels=["น้อย", "พอดี", "เยอะ"]) # คำถาม 1: GPA เฉลี่ยแตกต่างตามการนอนมั้ย? print("--- Q1: GPA ตามการนอน ---") print(df.groupby("sleep_category", observed=True)["gpa"].agg(["mean", "std", "count"]).round(2)) # คำถาม 2: นักศึกษาแต่ละสาขามีกี่คน? print("\n--- Q2: จำนวนต่อสาขา ---") print(df["major"].value_counts()) # คำถาม 3: GPA เฉลี่ยของแต่ละสาขา print("\n--- Q3: GPA ต่อสาขา ---") print(df.groupby("major")["gpa"].mean().round(2).sort_values(ascending=False))

4️⃣ Visualize — 1 คำถาม = 1 กราฟ

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# Q1
gpa_by_sleep["mean"].plot(kind="bar", ax=axes[0], color="#3776ab")
axes[0].set_title("GPA เฉลี่ยตามการนอน")
axes[0].set_ylabel("GPA")

# Q2
spend_by_cat.plot(kind="barh", ax=axes[1], color="#ffd43b")
axes[1].set_title("ค่าใช้จ่ายแต่ละหมวด")

# Q3
pm_by_day.plot(kind="line", ax=axes[2], marker="o", color="#ec4899")
axes[2].set_title("PM2.5 ตามวันในสัปดาห์")

plt.tight_layout()
plt.savefig("report.png", dpi=150)
plt.show()

🤖 ตรวจ AI ก่อนเชื่อ — Data Verification

AI "พูดมั่นใจ" ทั้งที่ผิด · ในงาน data — บางทีต่างกัน 1 บรรทัด = ผิดทั้ง report · สัปดาห์นี้ฝึก "distrust + verify"

🚨 5 รูปแบบ Hallucination ของ AI ใน Data Work

รูปแบบตัวอย่างวิธีจับ
1. Made-up numbers "p-value = 0.043, statistically significant" — ไม่เคยรัน scipy เลย ขอ code ที่คำนวณ + รันด้วยตัวเอง · ห้ามรับตัวเลขที่ AI พิมพ์ตรง ๆ
2. Wrong column name df.groupby("Year") ทั้งที่จริงคือ "year" (lowercase) รัน — ถ้า KeyError = พังตรงไหน · ถ้าเงียบ ๆ = ระวังกว่า
3. dropna ที่ลบมากเกิน df.dropna() ลบ 40% โดยไม่บอก · n เหลือ 18 จาก 30 เช็ค len(df) ก่อน/หลัง · ทุก cleaning step
4. Off-by-one ใน aggregation bins=[0, 6, 8, 24] — เวลา 6 ชม. ตกหมวดไหน? · บางครั้ง AI เขียน right=False ลืม รัน value_counts() ของ category ใหม่ — รวมแล้วเท่า total เดิมมั้ย?
5. สรุปเกินที่ data รองรับ "นอนน้อยทำให้ GPA ลดลง" จาก n=30 · เป็น correlation ไม่ใช่ cause ดูหัวข้อ "Correlation ≠ Causation" ด้านล่าง

📐 Pattern 1: Data Validation — ใส่ assert ทุกขั้น

ก่อน analyze — เช็คก่อนว่า data ที่เรามี "เป็นไปตามที่คิด"

import pandas as pd import numpy as np np.random.seed(0) df = pd.DataFrame({ "id": [f"U{i}" for i in range(40)], "sleep_hr": np.random.normal(7, 1.5, 40).round(1), "gpa": np.random.normal(3.2, 0.4, 40).round(2), "major": np.random.choice(["MAE", "EE", "ChE"], 40), "age": np.random.randint(18, 25, 40), }) # === Validation checks — รันก่อน analyze === # 1. Row count assert len(df) == 40, f"คาดว่ามี 40 rows แต่ได้ {len(df)}" print(f"✅ row count: {len(df)}") # 2. Required columns required = ["id", "sleep_hr", "gpa", "major"] missing = [c for c in required if c not in df.columns] assert not missing, f"ขาด columns: {missing}" print(f"✅ columns ครบ: {required}") # 3. Range checks assert df["sleep_hr"].between(0, 24).all(), "sleep_hr ต้อง 0-24" assert df["gpa"].between(0, 4).all(), "gpa ต้อง 0-4" assert df["age"].between(15, 80).all(), "age ผิดธรรมชาติ" print("✅ range OK") # 4. No critical nulls critical = ["id", "major"] for col in critical: assert df[col].notna().all(), f"{col} มี null" print("✅ no critical nulls") # 5. Unique IDs assert df["id"].nunique() == len(df), "ID ซ้ำ" print("✅ ID unique") # 6. Expected categories valid_majors = {"MAE", "EE", "ChE", "IE", "ME"} unexpected = set(df["major"]) - valid_majors assert not unexpected, f"พบ major ที่ไม่คาด: {unexpected}" print(f"✅ majors ใน whitelist: {set(df['major'])}") print("\n🎉 ผ่าน validation ทุกข้อ — พร้อม analyze") print(df.describe().round(2))
ทำไม assert สำคัญ — fail fast ถ้า data ผิด · ให้ "พังตรงนี้" · ไม่ใช่ "ผ่านไปแล้วได้ report ผิด" · ดีกว่ารู้ทีหลังตอน demo

🔍 Pattern 2: Manual Cross-Check — เช็คด้วยมือ 3 rows

หลัง groupby/aggregate ทุกครั้ง · เลือก 3 rows สุ่ม · คำนวณด้วยมือ · เทียบกับ AI

import pandas as pd # Sample df = pd.DataFrame({ "name": ["Ploy", "Bank", "Mint", "Tum", "Aof"], "scores": [[85, 90, 78], [60, 70, 55], [92, 88, 95], [50, 60, 45], [78, 82, 70]], }) # AI's "เฉลี่ยของแต่ละคน": df["mean"] = df["scores"].apply(lambda s: sum(s) / len(s)) print("--- AI's output ---") print(df) # Manual cross-check — Ploy ควรเฉลี่ย (85+90+78)/3 = 84.33 print("\n--- Manual check ---") print(f"Ploy: (85+90+78)/3 = {(85+90+78)/3:.2f} (AI: {df.loc[0, 'mean']:.2f})") print(f"Bank: (60+70+55)/3 = {(60+70+55)/3:.2f} (AI: {df.loc[1, 'mean']:.2f})") print(f"Mint: (92+88+95)/3 = {(92+88+95)/3:.2f} (AI: {df.loc[2, 'mean']:.2f})") # ถ้าทั้ง 3 ตรง = น่าจะถูกทั้งหมด # ถ้าผิด = bug แน่ ๆ — ต้องหาให้เจอ

⚠️ Pattern 3: Correlation ≠ Causation

❌ "นักศึกษาที่นอน < 6 ชม. ได้ GPA ต่ำกว่า → นอนน้อยทำให้ GPA ลด"
นี่คือ correlation · ไม่ใช่ causation · อาจเป็นเพราะ:
  • ทิศทางตรงข้าม: GPA ต่ำ → เครียด → นอนไม่หลับ (ไม่ใช่นอนน้อยทำ)
  • Confounding variable: งานพิเศษ → ทั้งนอนน้อย ทั้งไม่มีเวลาเรียน
  • Selection bias: เฉพาะคนที่ตอบ Form (ปกตินอนเยอะกว่าค่าเฉลี่ย)
  • Small sample: n=30 — coincidence ได้ง่าย
→ ในการเขียน insight: "คนนอนน้อยมี GPA ต่ำกว่าโดยเฉลี่ย (correlation)" ไม่ใช่ "นอนน้อยทำให้ GPA ลด (causation)"

📊 Pattern 4: ดูกราฟก่อน trust ตัวเลข

Anscombe's Quartet — 4 datasets ที่ "mean / variance / correlation เหมือนกันทุกตัว" แต่กราฟต่างกันโดยสิ้นเชิง · บางอันมี outlier · บางอันไม่ใช่ linear · กราฟแสดงสิ่งที่ summary ตัวเลขไม่บอก

หลัง groupby ทุกครั้ง → ทำ plt.scatter หรือ boxplot ดูก่อนเชื่อ mean

📏 Pattern 5: ระบุข้อจำกัดใน Report — แม้ AI จะไม่บอก

ข้อจำกัดต้องเขียนใน report
Sample size เล็ก (n < 100)"ผลนี้จากตัวอย่าง n=X เท่านั้น · อาจไม่สะท้อนประชากรทั้งหมด"
Self-selection bias"เฉพาะคนที่ยินดีตอบ Form · อาจ skew"
Self-report"ข้อมูลจากการรายงานตนเอง · อาจเอียงไปทาง 'น่าจะดูดี'"
Time window"เก็บช่วง X-Y · อาจไม่สะท้อนช่วงสอบ/ปิดเทอม"
Missing demographic mix"คนตอบ 90% เป็น MAE · ไม่สะท้อนสาขาอื่น"
กฎเหล็ก — "Insight ทุกข้อต้องมาคู่กับข้อจำกัด" ระดับมืออาชีพ คือ "รายงานสิ่งที่ data บอก + บอกว่าอะไรที่ data ไม่บอก"

5️⃣ Tell Story — Insight Report

Template — Insight Report

# Data Story: [หัวข้อ]

## คำถาม
1. ...
2. ...
3. ...

## ที่มาของข้อมูล
- จาก: [Google Form / data.go.th / etc.]
- ขนาด: [กี่ records, period ไหน]
- ข้อจำกัด: [bias / sample size / coverage]

## ผลที่พบ
1. **[คำถาม 1]** → [ตัวเลข + กราฟ + ความหมาย 2-3 ประโยค]
2. ...
3. ...

## ที่ "ทำให้แปลกใจ"
[1 ประโยค — สิ่งที่ไม่คาด]

## ข้อจำกัด
- [bias ที่อาจมี]
- [confounding variable ที่ไม่ได้คุม]

## ใช้กับใคร / ทำต่อยังไง
- [ใครได้ประโยชน์จาก insight นี้]
- [ถ้ามีเวลาอีก จะทำอะไรต่อ]

🧪 Workshop — Full Pipeline

  1. เลือกหัวข้อ + วิธีเก็บข้อมูล — ภายในวันแรก
  2. เก็บข้อมูลให้ได้ ≥ 30 records — ถ้า Form ก็ส่งให้เพื่อน · ถ้า dataset ก็ download
  3. เขียน 3 คำถาม ที่อยากตอบ ลงใน questions.md ก่อนเริ่ม analyze (อย่าให้ data ชี้นำ)
  4. เปิด Jupyter/Colab ทำ data cleaning · เซฟ clean.csv
  5. ตอบ 3 คำถามด้วย pandas + วาดกราฟแต่ละคำถาม
  6. เซฟกราฟเป็น report.png หรือ charts/folder
  7. เขียน Insight Report ตาม template (1 หน้า A4)
  8. Demo กับเพื่อน 1 คน · บันทึก feedback

🎯 Rubric — ประเมิน Data Project

เกณฑ์คะแนนมาตรฐาน
Data collection20≥ 30 records จริง · มี source ที่ verify ได้
Data cleaning15handle missing + duplicate + type · log ใน notebook
Analysis20ตอบ 3 คำถามถูก · มี groupby/agg
Visualization20≥ 3 กราฟ · มี title/label · เลือกชนิดถูก
Insight15เห็น pattern · เขียนเป็นภาษาคนได้
Limitations10ระบุ bias / sample / confound

ข้อผิดที่พบบ่อย

เริ่มจากข้อมูล → หาเรื่องที่จะเล่า — ตรงข้าม · "เริ่มจากคำถาม" ก่อน · ข้อมูลที่หาเอามาตอบคำถาม
เห็น correlation = สรุปว่า cause — "นักศึกษาที่นอน 8 ชม. GPA สูง" ≠ "นอนเยอะทำให้ GPA สูง" · อาจมี confound (มีเวลาเรียน, ครอบครัว, สุขภาพ)
n=5 แล้วสรุป trend — sample เล็กเกินไป trend ไม่ชัด · ระบุ "ข้อจำกัด" ใน report เสมอ

ส่งงานสัปดาห์นี้

Reference จาก slide เดิม

ครอบคลุม Topic 10 — Pandas + Stat ครบ + ขยายเป็น end-to-end project ตาม Mainidea