วิธีแก้ Notebook ในงาน Data Science 'ตาย': 6 นิสัยสร้างโค้ดที่รันได้ยาวนาน

Data Science Notebooks That Survive

Notebook จะ "ตาย" ทันทีที่ปุ่ม "Restart Kernel and Run All" ใช้งานไม่ได้

บ่อยครั้งที่ไม่มีใครสังเกตเห็นเรื่องนี้เป็นสัปดาห์ หลังจากวิเคราะห์เสร็จ แผนภูมิถูกใส่ในสไลด์ และไฟล์ถูกส่งขึ้นระบบเรียบร้อย แต่เมื่อมีคนถามถึงที่มาของตัวเลข แล้วคุณเปิด Notebook ขึ้นมารันใหม่ cell ที่ 12 อาจฟ้อง KeyError ในคอลัมน์ที่คุณเคยเปลี่ยนชื่อไปใน cell ที่ 31 และลบทิ้งไปใน cell ที่ 44 แต่ผลลัพธ์เดิมยังค้างอยู่ ทำให้ Notebook ดูเหมือนปกติทั้งที่รันไม่ได้แล้ว

การสร้างนิสัยเพื่อป้องกันปัญหานี้มีต้นทุนที่ต่ำมาก โดยเราจะนำเทคนิคทั้งหมดมาใช้กับชุดข้อมูลจริง และควบคุมให้โค้ดทั้งหมดอยู่ภายใต้ 100 บรรทัดของ Pandas

Data Science Notebooks That Survive

ข้อมูล (The Data)

ในบทความนี้ เราจะใช้ตาราง olympics_athletes_events จาก คำถามสัมภาษณ์ นี้

ข้อมูลประกอบด้วยนักกีฬา 336 คน จากการแข่งขัน 15 ครั้ง รวม 352 แถว ซึ่งมีรายละเอียดสำคัญคือคอลัมน์เหรียญรางวัล (medal) ที่มีข้อมูลอยู่ 120 แถว ส่วนช่องว่างหมายถึงนักกีฬาไม่ได้รับเหรียญในรายการนั้นๆ

ตัวอย่างข้อมูล:

idnamesexageheightteamnocyearsportmedal
3520Guillermo J. AmparanMMexicoMEX1924Athletics
35394Henry John FinchettMGreat BritainGBR1924Gymnastics
21918Georg Frederik Ahrensborg ClausenM28.0DenmarkDEN1924Cycling
110345Marinus Cornelis Dick SigmondM26.0NetherlandsNED1924Football
999998John TestmanM30.0180.0CanadaCAN2004AthleticsBronze

นิสัยที่ 1: รวมการตั้งค่าทั้งหมดไว้ใน Cell แรก

ทุกค่าคงที่ ไม่ว่าจะเป็น path, seed, threshold หรือ magic number ควรอยู่ใน cell แรกเพียงที่เดียวและไม่มีโค้ดส่วนอื่นปน

from pathlib import Path
import pandas as pd
 
DATA_PATH = Path("olympics_athletes_events.csv")
RANDOM_SEED = 42
 
NATURAL_KEY = ["id", "games", "event"]
SENTINEL_ID_FLOOR = 900_000   # real athlete ids in this extract stop at 134205
VALID_SEXES = {"M", "F"}
VALID_MEDALS = {"Gold", "Silver", "Bronze"}
AGE_RANGE = (10, 75)
HEIGHT_RANGE_CM = (120, 230)
WEIGHT_RANGE_KG = (25, 220)
MIN_ATHLETES_PER_SPORT = 5

วิธีนี้ช่วยให้ผู้ที่มาอ่าน Notebook ในภายหลังเห็นข้อสันนิษฐานทั้งหมดได้ทันทีโดยไม่ต้องเลื่อนหน้าจอ และเมื่อต้องแก้ไขค่าต่างๆ ก็มีจุดให้จัดการเพียงจุดเดียวเท่านั้น

Data Science Notebooks That Survive

นอกจากนี้ ค่า seed มีความสำคัญอย่างยิ่งต่อความน่าเชื่อถือ ไม่ว่าจะเป็นการใช้ df.sample(), train/test split หรือ k-means เพราะ Notebook ที่ให้ผลลัพธ์ต่างกันทุกครั้งที่รันจะขาดความน่าเชื่อถือทันที

นิสัยที่ 2: เขียนหนึ่งฟังก์ชันต่อหนึ่ง Cell

กฎสำคัญคือ หนึ่ง cell ต้องทำหน้าที่เพียงอย่างเดียว คือนิยามฟังก์ชันหรือเรียกใช้ฟังก์ชัน ห้ามทำทั้งสองอย่าง และห้ามแก้ไขตัวแปรที่สร้างจาก cell อื่นโดยตรง

def load_raw(path: Path) -> pd.DataFrame:
    """Read the Olympics CSV with no cleaning applied."""
    return pd.read_csv(path)
 
def clean(df: pd.DataFrame) -> pd.DataFrame:
    """Drop test rows and duplicate entries, then encode 'no medal' explicitly."""
    out = df[df["id"] < SENTINEL_ID_FLOOR].copy()
    out = out.drop_duplicates(subset=NATURAL_KEY, keep="first")
    out["medal"] = out["medal"].fillna("None")
    out["sex"] = out["sex"].astype("category")
    out["season"] = out["season"].astype("category")
    return out.reset_index(drop=True)
 
def add_features(df: pd.DataFrame) -> pd.DataFrame:
    """Add decade, is_medalist and bmi. Never mutates the input frame."""
    out = df.copy()
    out["decade"] = (out["year"] // 10) * 10
    out["is_medalist"] = out["medal"].ne("None")
    out["bmi"] = bmi(out["weight"], out["height"])
    return out

การใช้ .copy() ในบรรทัดแรกของฟังก์ชันเป็นเทคนิคสำคัญที่ป้องกันไม่ให้เกิดการเขียนทับข้อมูลเดิม ทำให้ลำดับการรัน cell ไม่ส่งผลเสียต่อผลลัพธ์ และช่วยแก้ปัญหาคลาสสิกที่การรันครั้งที่สองให้ผลต่างจากครั้งแรก

นิสัยที่ 3: ตรวจสอบความถูกต้องของข้อมูล (Validation)

อย่าเชื่อใจข้อมูลจนกว่าจะได้รับการตรวจสอบ ให้เขียนเงื่อนไขที่คุณคาดหวังลงไปเพื่อให้ Notebook ช่วยยืนยันความถูกต้อง

def validate_raw(df: pd.DataFrame) -> list[str]:
    """Return a list of contract violations. An empty list means the data is usable."""
    problems = []
 
expected = {"id", "sex", "age", "height", "weight", "year", "sport", "event",
                "medal", "games", "team"}
    missing = expected - set(df.columns)
    if missing:
        problems.append(f"missing columns: {sorted(missing)}")
        return problems
 
dupes = df.duplicated(subset=NATURAL_KEY).sum()
    if dupes:
        problems.append(f"{dupes} duplicate rows on {NATURAL_KEY}")
 
sentinels = df.loc[df["id"] >= SENTINEL_ID_FLOOR, "id"]
    if len(sentinels):
        found = sorted(int(i) for i in sentinels.unique())
        problems.append(f"{len(sentinels)} sentinel ids: {found}")
 
bad_sex = set(df["sex"].dropna().unique()) - VALID_SEXES
    if bad_sex:
        problems.append(f"unexpected sex values: {bad_sex}")
 
bad_medal = set(df["medal"].dropna().unique()) - VALID_MEDALS
    if bad_medal:
        problems.append(f"unexpected medal values: {bad_medal}")
 
for col, (lo, hi) in [("age", AGE_RANGE),
                          ("height", HEIGHT_RANGE_CM),
                          ("weight", WEIGHT_RANGE_KG)]:
        out = df[col].dropna()
        n = ((out < lo) | (out > hi)).sum()
        if n:
            problems.append(f"{n} {col} values outside {lo}-{hi}")
 
return problems

Data Science Notebooks That Survive

ตัวอย่างเช่น การตรวจสอบข้อมูลซ้ำโดยใช้ Natural Key (id, games, event) จะช่วยป้องกันการลบข้อมูลที่ถูกต้องออกไป ในขณะที่การเช็ค Sentinel IDs ช่วยดักจับข้อมูลทดสอบ (เช่น "John Testman") ที่อาจปนมาและทำให้ค่าสถิติผิดเพี้ยนไปถึง 13%

นิสัยที่ 4: เขียนการทดสอบ (Tests) ไว้ใน Notebook

คุณสามารถใช้การทำ Assertion ง่ายๆ เพื่อตรวจสอบฟังก์ชันได้โดยไม่ต้องพึ่งพาเครื่องมือภายนอกอย่าง pytest

def _fixture() -> pd.DataFrame:
    return pd.DataFrame({
        "id": [1, 1, 2],
        "games": ["1924 Summer"] * 3,
        "event": ["Rings", "Rings", "Rings"],
        "sex": ["M", "M", "F"],
        "age": [24.0, 24.0, None],
        "height": [180.0, 180.0, 165.0],
        "weight": [81.0, 81.0, 55.0],
        "year": [1924, 1924, 1924],
        "season": ["Summer"] * 3,
        "sport": ["Gymnastics"] * 3,
        "medal": ["Gold", "Gold", None],
        "team": ["Denmark"] * 3,
    })
 
def run_tests() -> None:
    fx = _fixture()
 
assert len(clean(fx)) == 2, "clean() must drop the duplicate entry"
    assert clean(fx)["medal"].tolist() == ["Gold", "None"], "missing medal becomes 'None'"
 
before = fx.copy()
    add_features(fx)
    pd.testing.assert_frame_equal(fx, before)   # add_features must not mutate
 
feat = add_features(clean(fx))
    assert feat["is_medalist"].tolist() == [True, False]
    assert round(feat.loc[0, "bmi"], 1) == 25.0
    assert feat["decade"].unique().tolist() == [1920]
 
assert validate_raw(fx) == [f"1 duplicate rows on {NATURAL_KEY}"]
    assert "missing columns" in validate_raw(pd.DataFrame({"id": [1]}))[0]
 
planted = pd.concat([fx, fx.iloc[[2]].assign(id=999999)], ignore_index=True)
    assert any("sentinel" in p for p in validate_raw(planted))
    assert 999999 not in clean(planted)["id"].values
 
print("all 10 checks passed")

การรัน assertions เหล่านี้ช่วยให้คุณรู้ตัวทันทีหากโค้ดที่แก้ไขใหม่ไปทำลายตรรกะเดิม ซึ่งดีกว่าการไปพบข้อผิดพลาดในภายหลังตอนที่ต้องส่งงาน

นิสัยที่ 5: เขียนเอกสารที่ตรวจสอบได้จริง

ใช้ doctest เพื่อให้ตัวอย่างในเอกสารกำกับโค้ด (Docstring) ถูกรันและตรวจสอบความถูกต้องอยู่เสมอ

def bmi(weight_kg: float, height_cm: float) -> float:
    """Body mass index in kg/m2.
 
>>> round(bmi(81.0, 180.0), 1)
    25.0
    >>> round(bmi(55.0, 165.0), 1)
    20.2
    """
    return weight_kg / (height_cm / 100) ** 2
 
import doctest
doctest.run_docstring_examples(bmi, globals(), name="bmi", verbose=True)

นิสัยที่ 6: ทำให้ Notebook รันเป็น Script ได้

การใช้บล็อก if __name__ == "__main__" ใน cell สุดท้าย จะช่วยให้ Notebook สามารถถูกแปลงเป็น script และถูก import ไปใช้งานในที่อื่นได้โดยไม่เกิดผลกระทบข้างเคียง

def main() -> pd.DataFrame:
    raw = load_raw(DATA_PATH)
    problems = validate_raw(raw)
    df = add_features(clean(raw))
 
print("\nspot check:")
    print(df.sample(3, random_state=RANDOM_SEED)[["name", "year", "sport", "medal", "bmi"]])
 
report = medal_rate_by_sport(df)
    return report
 
if __name__ == "__main__":
    run_tests()
    doctest.run_docstring_examples(bmi, globals(), name="bmi", verbose=True)
    main()

บทสรุป

การสละเวลาเพิ่มเพียงเล็กน้อยเพื่อสร้างนิสัยเหล่านี้ จะช่วยให้ Notebook ของคุณยังคงให้ผลลัพธ์ที่ถูกต้องแม้เวลาจะผ่านไปนานเพียงใด

Data Science Notebooks That Survive

จำไว้ว่าหากคุณกด Restart kernel และ Run all แล้วมันยังทำงานได้ถูกต้อง นั่นหมายความว่า Notebook ของคุณยังมีชีวิตอยู่และพร้อมใช้งานเสมอ

Data Science Notebooks That Survive

Source: KDnuggets
ดูแลงานแปลและเรียบเรียงโดย SirilukP

ความคิดเห็น (0)

เข้าสู่ระบบเพื่อร่วมแสดงความเห็น

สมัครสมาชิก

มาเป็นคนแรกที่แสดงความเห็นกันเลยโบร