Tag: Data Analyst

  • Main Series: Part 2 เข้าใจ Classification ใน Supervised Learning

    Main Series: Part 2 เข้าใจ Classification ใน Supervised Learning

    และแล้วก็ผ่านมาได้ 2 อาทิตย์แล้วนะครับ หลังจากที่ผมได้เขียนเกี่ยวกับ Linear Regression ในบทความก่อนหน้าไป

    ผมว่าเพื่อนๆก็คงอยากจะรู้แล้วว่า Model ที่เหลืออยู่ของ Supervised Learning เนี้ยคืออะไรบ้าง เพราะฉะนั้นผมว่า นี้ก็คงจะเป็นฤกษ์งามยามดีที่เราจะเปิด post ใหม่แล้วละครับ

    หลักจากที่ Blog ที่แล้วเราได้เรียนรู้เกี่ยวกับ Linear Regression ทั้ง Simple และ Complex ไปแล้ว วันนี้เรามาดูอีกเทคนิคที่นิยมทำเพื่อแยกประเภท หรือแบ่งออกเป็น Classes อย่าง Classification กันดีกว่าครับ

    คุณเคยมีประสบการณ์ที่ต้องเดาหรือเลือกคำตอบ yes/no ใช่/ไม่ใช่ ไหมครับ หรือ ในตอนที่คุณรู้สึกอยากจะเปลี่ยนโปรโทรศัพท์ อยู่ๆค่ายมือถือก็เลือกโทรมาหาคุณเพื่อขายโปรโมชั่น คุณเคยสงสัยไหมว่าพวกเขารู้ได้อย่างไร ติดกล้องในห้องเราหรือเปล่านะ หรือบางทีคุณอาจจะคิดว่ามันก็แค่ บังเอิญ ?

    พูดกันตามตรงข้อมูลเหล่านี้มันมีเบื้องลึกเบื้องหลังมากกว่านั้น และวันนี้ผมก็มีคำตอบมาให้พวกคุณได้คลายความสงสัยเสียที เมื่อพร้อมแล้วไปลุยกั๊นน

    Table of Content


    1. What is Classification ?
    2. General Classification Algorithms
    3. Let’s Implement them (Python Coding)
    4. Imbalance Dataset
    5. Evaluation
    6. Conclusion

    What is Classification

    Classification เป็นหนึ่งในประเภท Algorithms ภายใต้วิธีการสอนแบบ Supervised Learning โดยมีเป้าที่จะจัดหมวดหมู่ข้อมูล (Categorization) หรือ การทำนายกลุ่ม (Class Prediction) ที่เราต้องการทำนายค่า โดยใส่ค่าบางอย่างเข้าไปเช่น X (ข้อมูลดิบ) แล้วได้ค่า y (ประเภท) ออกมานั้นแหละคือ Classification Algorithm.

    สำหรับ Classification เองก็สามารถแบ่งย่อยได้เป็นสองกลุ่มใหญ่ได้แก่

    • Binary Classification: ประเภทที่ได้คำตอบออกมาสองค่า ก็คือ 0 กับ 1 เช่น Yes/No Spam/ Not Spam, Disease/ No Disease
    • Multi-class Classification: ประเภทที่จัดกลุ่มข้อมูลออกเป็นมากกว่าแค่ 2 ค่า ยกตัวอย่างเช่น ประเภทของสิ่งมีชีวิต แบ่งตาม Specie หรือ ตาม Phylum พวกผลไม้ และ พวกประเภทตัวเลขต่างๆ

    ก่อนที่เราจะไปดูว่าใน Classification มี Algorithm อะไรบ้าง เรามาดูนิยามคำศัพท์กันก่อน เพื่อที่เมื่อเข้า Session ถัดไปแล้วก็จะได้ไม่งงกันครับ

    ระระรู้ได้ไงกันว่าฉันกำลังจะยกเลิกโปรมือถือ
    คำศัพท์ความหมาย
    Features/Independent Variables (ตัวแปรอิสระ)คุณลักษณะต่างๆ ของข้อมูล ที่เราใช้เป็น ‘วัตถุดิบ’ ในการทำนาย เช่น อายุ, เพศ, จำนวนบุหรี่ที่สูบ (X)
    Labels/Classes/Dependent Variables (ตัวแปรตาม)ตัวแปรที่จะได้รับผลกระทบหากเราเปลี่ยนแปลงวัตถุดิบ y (ผลลัพธ์ที่เราต้องการทำนาย, ประเภท)
    Training Data, Testing Dataข้อมูลสำหรับ ‘สอน’ ให้โมเดลฉลาด และ ข้อมูลสำหรับ ‘ทดสอบ’ ว่าโมเดลที่เราสอนมานั้นเก่งแค่ไหน
    Algorithmสูตร หรือ กระบวนการ’ที่ใช้ในการเรียนรู้จากข้อมูลเพื่อสร้าง ‘โมเดล’ ขึ้นมา
    Predictionการทำนาย หรือการคาดคะเน
    Churn Analysisการทำนายลักษณะของลูกค้าที่มีแนวโน้มกำลังจะยกเลิกบริการหรือมีแนวโน้มจะไม่กลับมาซื้อสินค้าหรือบริการซ้ำอีก

    General Classification Algorithm

    สำหรับการทำ Classification Supervised Learning แล้ว ในปัจจุบันมีอัลกอริทึมหลายแบบมาก ซึ่งแต่ละอัลกอริทึมก็มีจุดแข็งและจุดอ่อนของตัวเองแตกต่างกันออกไป โดยขึ้นอยู่กับวิธีใช้ของผู้ใช้งาน ทั้งนี้เราก็ควรจะรู้จักแต่ละตัวไว้บ้างเพื่อให้เราไม่ติดกับดับ No Free Lunch ตามที่เราได้กล่าวไปในบทความก่อนหน้านี้

    มาเริ่มกันที่ตัวแรกและถือว่าเป็นตัวพื้นฐานสุดเลยอย่าง Logistic Regression

    ทุกคนอาจจะสงสัยทำไมชื่อมันคล้ายกับตัวก่อนหน้าอย่าง Linear Regression เลย สองอัลกอริทึมนี้เป็นอะไรกันหรือเปล่า ? คำตอบก็คือ ใช่ครับ! สองตัวนี้มีความเกี่ยวข้องกันนั้นก็คือใช้พื้นฐานทางสมการทางคณิตศาสตร์เหมือนกัน นั้นคือสมการเส้นตรง แต่วัตถุประสงค์แตกต่างกันอย่างสิ้นเชิงเลยครับ

    Linear Regression: ใช้ทำนายค่าตัวเลขที่ต่อเนื่องไปเรื่อยๆ (เช่น ราคาบ้าน, ยอดขาย)

    Logistic Regression: ใช้ทำนายความน่าจะเป็นเพื่อ “จัดกลุ่ม” หรือ “ตัดสินใจ” ว่าข้อมูลนั้นควรเป็นคลาสไหน (เช่น ใช่/ไม่ใช่, ป่วย/ไม่ป่วย, สแปม/ไม่ใช่สแปม)

    Logistic Regression

    แต่มีจุดต่างตรงที่ Logistic Regression จะนำ สมการเส้นตรงไปจำกัดขอบเขต ด้วย Sigmoid Function หรือ Logistic Function เพื่อให้ผลลัพธ์อยู่ในช่วง 0 ถึง 1 เสมอ

    เมื่อข้อมูลเราอยู่ระหว่าง 0 ถึง 1 นั้นหมายความว่ายิ่งข้อมูลมีค่าเข้าใกล้ 1 มากเท่าไหร่นั้นแปลว่าข้อมูลเหล่านั้นก็จะสามารถติดอยู่ในคลาส ใช่ หรือ 1 นั้นเอง

    ในสมการของ Sigmoid Function เพื่อนๆอาจจะสงสัยว่าตัว e คืออะไร ตัว e เป็นค่าคงที่ในวิชาคณิตศาสตร์ที่เรียกว่า Euler Value ซึ่งจะมีค่าเท่ากับ 2.71828 นั้นเอง

    Sigmoid Function

    ข้อดี (Pros) 👍

    • เข้าใจง่าย สามารถเขียนเป็นรูปสมการและอธิบายได้อย่างเข้าใจ
    • ผลลัพธ์ตีความได้ (แปลงออกมาเป็นความน่าจะเป็นได้)
    • ประสิทธิภาพที่ดีและสามารถใช้เป็น Baseline ของการทำ Binary Classification

    ข้อเสีย (Cons) 👎

    • อ่อนไหวต่อ Outliers: ข้อมูลที่โดดไปจากกลุ่มมากๆ อาจส่งผลกระทบต่อการลากเส้นตัดสินใจได้
    • หากข้อมูลที่มี Features เยอะอาจจะทำได้อย่างไม่เต็มประสิทธิภาพ

    def logistic_regression_model(df_X_scaled, y):
        X_trained, X_test, y_trained, y_test = train_test_split(df_X_scaled, y, test_size = 0.2, train_size=0.8, random_state = 42)
    
        lr = LogisticRegression(random_state = 42, max_iter= 1000)
        lr.fit(X_trained, y_trained)
    
        evaluation_model('Logistic Regression',lr,y_test, X_test)
        return
    

    Decision Tree

    Photo by vee terzy on Pexels.com

    อัลกอริทึมที่สองสำหรับการทำ Classification นั้นคือ Decision Tree (ต้นไม้ตัดสินใจ) ครับ อันที่จริงแล้วโมเดลนี้สามารถใช้ร่วมกันระหว่าง Regression หรือ Classification ก็ได้ โดยมีวัตถุประสงค์เพื่อที่จะทำนายค่า y โดยอ้างอิงวิธีที่มนุษย์ใช้ในการตัดสินใจ โดยแบ่งการตัดสินใจเป็นเงื่อนไขเล็กๆ เช่น สมมติเราจะซื้อของ 1 ชิ้น สมองก็จะค่อยๆแบ่งออกเป็นคำถามเล็กเพื่อตอบคำถามหลัก​ (ซื้อหรือไม่ซื้อ) เป็น สเปคดีไหม ? สีใช่ที่ชอบไหม ? ซื้อมาเอาไปทำไร ? คุ้มไหม ? เป็นต้น

    ข้อดี

    • ง่ายต่อการเข้าใจ สามารถสร้างเป็นกราฟได้
    • เตรียม data ง่าย บางครั้งแทบไม่ต้องใส่ข้อมูลในช่วงที่หายไป หรือเอา NULL ออกก็ได้ (แต่เราควรทำให้ข้อมูลสะอาดที่สุดนะครับเพื่อลดการผิดพลาดของการนำไปใช้)
    • ทำงานได้ดีมากกับข้อมูลที่มีความหลากหลายในด้าน features
    • ค่า Cost สำหรับการรัน Model. จะเป็น log(n) สำหรับจำนวน data ที่ใช้สอน โมเดล ส่งผลให้สามารถทดสอบกับข้อมูลจำนวนมากได้

    ข้อเสีย

    • การทำนายของ Decision Tree จะไม่ได้อยู่ในลักษณะที่เป็นตัวเลข ต่อเนื่อง Continuous number แต่จะเป็นค่าคงที่เฉพาะเอง ซึ่งนั้นหมายความว่าตัวโมเดลไม่เหมาะที่จะใช้ทำนายข้อมูลที่ต้องการดูแนวโน้มหรือ เทรนด์ (Extrapolation)
    • ด้วยความที่บางครั้ง Decision Tree ชอบสร้าง แขนงการตัดสินใจที่ซับซ้อนจนมากเกินไป อาจเกิดเหตุการณ์ Overfitting ได้ ซึ่งเราอาจจะเปลี่ยนไปใช้ Random Forest แทนเพื่อแก้ปัญหานี้
    • หากข้อมูลมี Outlier ผิดปกติเยอะอาจส่งผลต่อโครงสร้างของ Decision Tree นำไปสู่การทำนายที่ให้ผลลัพธ์ที่ผิดปกติได้
    def decision_tree_model(df_X_scaled, y):
        X_trained, X_test, y_trained, y_test = train_test_split(df_X_scaled, y, test_size = 0.2, train_size=0.8, random_state = 42)
    
        clf = DecisionTreeClassifier(random_state=42)
    
    
        clf.fit(X_trained,y_trained)
    
        evaluation_model('Decision Tree',clf, y_test, X_test)
        return
    
    

    Random Forest

    Random Forest

    อัลกอริทึม ถัดไปที่เป็นการต่อยอดจาก Decision Tree นั้นคือ Random Forest อัลกอรึทึมนี้เกิดจากการที่เรานำผลลัพธ์มากมายจาก Decision Trees หลายๆอัน และสรุปออกมาเป็นผลลัพธ์เดียว

    โดย Random Forest ถือว่าเป็นส่วนหนึ่งของ Ensumble Learning หรือ กลุ่มอัลกอริทึมที่เกิดจากการเทรนอัลกอริทึมเดิมซ้ำๆ หลายรอบ บนข้อมูลชุดเดียวกันจนได้ผลลัพธ์ โดยในแต่ละครั้งก็จะจัดสัดสวนข้อมูลที่ใช้เทรนไม่เท่ากันด้วย

    โมลเดลนี้สามารถนำไปใช้ได้ทั้งใน Regression หรือ Classification ก็ได้

    ข้อดี

    • ด้วยความที่ข้อมูลถูกเทรนซ้ำๆ ด้วยการแบ่งสัดส่วนที่ไม่เหมือนกัน (Bootstrap Aggregating (Bagging)) ส่งผลให้ความแม่นยำนั้นสูงเช่นกัน จินตนการเหมือนกับที่เราถามคำถามเดียวกันกับฝูงชน แล้วเราค่อยสรุปออกมาเป็นคำตอบเดียวจากหลายๆคำตอบนั้นเอง
    • จากที่ได้กล่าวใน โมเดลที่แล้วก็คือช่วยป้องกันการเกิด Overfitting
    • มีความหยืดหยุ่นมากในการทำ Hyper-parameters Tuning

    ข้อเสีย

    • ความยากในการตีความ (Interpretability) เพราะต้นไม้แต่ละค้นนั้นก็จะมีการเลือกใช้ Features ไม่เหมือนกัน (Feature Randomness)
    • มีต้นทุนสูงเนื่องจากว่าต้องผ่านการคำนวณหลายครั้ง
    def random_forest_model(df_X_scaled, y):
    
        X_trained, X_test, y_trained, y_test = train_test_split(df_X_scaled, y, test_size = 0.2, train_size=0.8, random_state = 42)
    
        rfc = RandomForestClassifier(random_state = 42)
        rfc.fit(X_trained, y_trained)
    
        evaluation_model('Random Forest',rfc, y_test, X_test)
        return
    

    K Nearest Neighbours

    อีกหนึ่งโมเดลที่เป็นที่นิยมและใช้งานง่ายมากๆ นั้นคือ knn หรือ K Nearest Neighbour โมเดลที่จะหาทำนายค่า จะดึงข้อมูลที่ใกล้เคียงตัวมันเองที่สุด K ตัว แล้วก็ค่อยดูว่าจาก K ตัวเป็น Class อะไรบ้าง

    KNN สามารถประยุกต์ใช้ได้ทั้งใน Classification หรือ Regression ก็ได้โดยที่

    • เราจะหาผลโหวตที่มากที่สุด หากเราทำ Classification
    • เราจะหาค่าเฉลี่ยของ K ทุกตัวหากเราทำ Regression เพื่อ Predict บางอย่าง

    ข้อดี

    • Implement ได้ง่าย และ Train ง่าย เพราะตัวอัลกอริทึมเองนั้นจำข้อมูลอย่างเดียวในช่วงที่ Train ข้อมูล ( Lazy Learner )
    • ใช้กับ Test Data หรือ Data ใหม่ที่โมเดลไม่เคยเห็นได้ดี

    ข้อเสีย

    • อัลกอริทึมนี้ sensitive มาก ถ้าเจอข้อมูลที่ Outlier เยอะๆ Missing Value, หรือ NaN ละก็เตรียมตัวระเบิดได้เลย
    • KNN ทำงานได้ช้ามากเมื่อมีข้อมูลเยอะ (Poor Scalability) เพราะทุกครั้งที่ ทำการทำนายมันจะต้องคำนวณระยะทุกๆจุด ใช่ครับมีล้านจุดก็ทำนายล้านที ต่างจากเพื่อนๆของมันที่ Train มาก่อนแล้ว
    • KNN จะทำได้ไม่ดีนักหากข้อมูลของเรามี Features เยอะมากๆ คุณลองคิดดูสิถ้าสมมติว่าข้อมูลมี 50 features มันก็จะดูห่างกันมาก ดูไม่ได้ใกล้เคียงกับใครเลย
    • การกำหนด K เหมือนจะง่าย แต่จริงๆ แล้วอาจจะให้ค่าที่ต่างกันหากไม่ระวัง
    def knn_model(df_X_scaled, y):
        X_trained, X_test, y_trained, y_test = train_test_split(df_X_scaled, y, test_size = 0.2, train_size=0.8, random_state = 42)
    
        knn = KNeighborsClassifier()
        knn.fit(X_trained,y_trained)
    
        evaluation_model('K-nearest neighbourhood', knn ,y_test, X_test)
        return
    

    Support Vector Machines

    สำหรับอัลกอริทึมสุดท้ายสำหรับบทความนี้ นั้นคือ อัลกอริทึมที่มีชื่อว่า SVM หรือ Support Vector Machines ซึ่งเป็นอัลกอที่มีความยืดหยุ่นมาก เมื่อข้อมูลมีความซับซ้อน หลาย Feature แต่จำนวน data ดันน้อย

    หลักการของ SVM คือการพยายามสร้าง ‘ถนน’ ที่กว้างที่สุดเท่าที่จะเป็นไปได้เพื่อแบ่งกลุ่มข้อมูลออกจากกัน โดยเส้นขอบถนนทั้งสองข้างก็คือ Support Vectors นั่นเอง

    ข้อดี

    • มีความยืดหยุ่นมาก
    • ทำงานได้ดีกับ ข้อมูลที่มี Dimensions เยอะแต่จำนวน data น้อย
    • สามารถจัดการกับข้อมูลที่ไม่เป็นเชิงเส้นได้ดีเยี่ยมด้วยเทคนิค Kernel Trick ถามว่า Kernel Trick คืออะไรมันก็คือการที่คุณพยายามจะแก้ปัญหาที่ไม่สามารถลาก เส้นตรงเส้นเดียวเพื่อแบ่งของได้ ยกตัวอย่างเช่น สมมติมี ถั่วสีน้ำเงินกับ สีเขียวอยู่บนโต๊ะปนๆกันอยู่ ถ้ามองก็จะเห็นเป็น 2 มิติใช่ไหมครับแยกไม่ได้เลย การใช้ Kernel Trick ก็เหมือนการที่คุณทุบใต้โต๊ะแล้วถั่วก็ลอยขึ้น อยู่ในมุมมอง 3 มิติ ส่งผลให้คุณสามารถใช้กระดาษ ( Hyperplane ) เพื่อสอดเข้าไประหว่างกลางเพื่อทำการแบ่ง นั้นแหละคือ Kernel Trick

    ข้อเสีย

    • อาจทำงานได้ไม่ดีกับชุดข้อมูลขนาดใหญ่มาก ๆ (เนื่องจากการคำนวณที่ซับซ้อน) หรืออาจต้องใช้เวลาในการปรับแต่ง Kernel ที่เหมาะสมเพื่อจัดการกับข้อมูลที่ไม่เป็นเชิงเส้น
    SVM
    def svc_model(df_X_scaled, y):
        X_trained, X_test,  y_trained, y_test = train_test_split(df_X_scaled, y, test_size = 0.2, train_size=0.8, random_state = 42)
        svc = SVC(random_state = 42, probability=True)
        svc.fit(X_trained, y_trained)
    
        evaluation_model('Support Vector Machines', svc,y_test, X_test)
        return
    

    หลังจากที่เราได้เรียนรู้อัลกอริทึมต่างๆ ขั้นตอนตอไปก็คือลองลงมือ Implement จริงดู ไปกันเล๊ยย

    Let’s Implement them w8 Python Code

    สำหรับรอบนี้ผมเลือกใช้ ข้อมูลชุดที่มีชื่อว่า framingham.csv สำหรับการ Train นะครับเป็นข้อมูลที่เกี่ยวกับคนไข้ที่ป่วยและมีแนวโน้มจะเป็นโรคหลอดเลือดหัวใจในระยะ 10 ปี หรือไม่

    ข้อมูลนี้จะประกอบไปด้วย 4,240 records และ 16 Columns เรามาลองดูแต่ละ Column กันดีกว่าครับ

    คำศัพท์ความหมาย
    maleระบุเพศของผู้ป่วย ชาย 1 หญิง 0
    ageอายุของผู้ป่วย
    educationระดับการศึกษามี (1-4)
    currentSmokerสถานะว่าสูบบุหรี่อยู่หรือไม่ 0 คือไม่สูบและ 1 คือยังสูบ
    cigsPerDayจำนวนบุหรี่ที่สูบในแต่ละวัน
    BPMedsระบุว่าผู้ป่วยรับประทานยาลดความดันโลหิต (1) หรือไม่ (0)
    prevalentStrokeระบุว่าผู้ป่วยมีประวัติเป็น Stroke หรือไม่ 1 คือเป็น 0 คือไม่
    prevalentHypระบุว่าผู้ป่วยมีประวัติเป็น Hypertension
    diabetesระบุว่าผู้ป่วยมีประวัติเป็นโรคเบาหวาน (1) หรือ ไม่ (0)
    totCholปริมาณโคเลสเตอรอลของผู้ปวย
    sysBPความดันโลหิตซิสโตลิกของผู้ป่วย
    BMIBMI ของผู้ป่วย
    heartRateอัตราการเต้นของหัวใจผู้ป่วย
    glucoseระดับน้ำตาลกลูโคสของผู้ป่วย
    TenYearCHDระบุผลลัพธ์ว่าผู้ป่วยมีแนวโน้มจะเป็นโรคหลอดเลือดหัวใจในระยะ 10 ปี หรือไม่
    diaBPความดันโลหิตไดแอสโตลิกของผู้ป่วย

    หลังจากรู้แล้วว่าแต่ละ Columns คือค่าอะไรเรามาลุยกันเลยดีกว่าครับทุกคน โดยเป้าหมายของเราก็คือ การสร้างโมเดลเพื่อทำนายความเสี่ยงของการเกิดโรคหลอดเลือดหัวใจในอีก 10 ปีข้างหน้าให้ได้ ป่ะลุยกัน 🔥

    Preprocessing

    ขั้นแรกเราก็ต้องโหลด dataset กันก่อนด้วย Library Pandas

    df = pd.read_csv('framingham.csv', decimal=',', sep=',', header =0)
    

    หลังจากนั้นก็ทำการ drop Null Value ออกด้วย dropna()

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

    วิธีการที่ดีกว่าในกรณีเช่นนี้ ก็คือการที่เราแทนค่าลงไปด้วย Mean/Meadian Imputation จะช่วยให้โมเดลทำงานได้ดีกว่าครับ

    df.dropna(inplace=True)
    

    ลองตรวจว่าข้อมูลผลลัพธ์ผู้ป่วยกระจายตัวแบบใด

    print(df['TenYearCHD'].value_counts(normalize=True))
    
    

    ทำไมต้องใส่ normalize = True ?

    เพราะว่า โดยปกติแล้ว value_counts จะคืนค่าที่มีมากที่สุดใน columnที่เราเลือกก่อนเสมอและอยู่ในลักษณะของความถี่ธรรมดา อย่างไรก็ตาม หากเราต้องการจะเปรียบเทียบว่าข้อมูลมันเอียงไปทางใดทางหนึ่งหรือไม่นั้น (ข้อมูลไม่ใช่กระจายตัวสม่ำเสมอ) การใส่ normalize = True จะแปลงตัวเลขให้เป็นความถี่สัมพัทธ์ มีค่า [0,1] แทน ซึ่งง่ายต่อการตรวจสอบและทำความเข้าใจ

    เดี๋ยวเราจะกลับมาดูค่านี้กันไหมนะครับ ไปดูตรงอื่นๆกันก่อน

    เนื่องจาก .csv ที่ผมโหลดมา ดันให้ sysBP เป็น object type ซะงั้นผมก็เลยต้องแปลงค่าเสียก่อน

    df['sysBP'] = pd.to_numeric(df['sysBP'])
    

    หลังจากนั้นเราก็มาทำการ clean outlier กันต่อครับ โดยผมเลือกใช้วิธี เช็คด้วย IQR (Inter Quartile Range) และแทนค่าค่าที่เกินขอบเขตแทนที่จะลบออกครับ เพราะ Dataset จะหายไปราวๆ 15% หรือ 800 rows เลยทีเดียวถ้าเราไม่เก็บค่าพวกนี้ไว้ ซึ่งอาจส่งผลต่อ metrics ต่างๆได้

      features = ['male', 'age', 'cigsPerDay' , 'totChol', 'sysBP', 'glucose' ]
        y = df['TenYearCHD']
        X_processed = df[features].copy()
        for col in features:
                X_processed[col] = cap_outliers_iqr(X_processed[col])
    
    def cap_outliers_iqr(df_column):
    
        q1 = df_column.quantile(0.25)
        q3 = df_column.quantile(0.75)
        IQR = q3 - q1
        lower_bound = q1 - 1.5*IQR
        higher_bound = q3+ 1.5*IQR
    
        capped_column = np.where(df_column < lower_bound, lower_bound, df_column)
    
        capped_column = np.where(capped_column > higher_bound, higher_bound, capped_column)
        return pd.Series(capped_column, index = df_column.index)
    

    หลังจากนั้นเราก็มาทำการแปลง scale ข้อมูลด้วย StandardScaler เพื่อให้ข้อมูลอยู่บนบรรทัดฐานเดียวกันป้องกันการทำนายผิดพลาด

        scaler = StandardScaler()
        X_scaled = scaler.fit_transform(X_processed)
        f_X_scaled = pd.DataFrame(X_scaled, columns =X_processed.columns)
    

    StandardScaler เป็นหนึ่งใน feature scaling เทคนิคที่จะทำให้ข้อมูลของเรามีค่าเฉลี่ย เป็น 0 และ มีค่าการกระจายตัวของข้อมูลเป็น 1 ตามหลักการกระจายตัวปกติ วิธีนี้เป็นวิธีที่เหมาะต่ออัลกอริทึมอย่าง SVM และ Logistic Regression เพราะทั้งคู่ต้องการให้ข้อมูลอยู่ในลักษณะกระจายตัวปกติ

    ถึงแม้ว่าการทำ Scaling จะไม่ส่งผลโดยตรงต่อประสิทธิภาพของโมเดลประเภท Tree-based อย่าง Decision Tree หรือ Random Forest แต่การทำไว้ก็ถือเป็น Good Practice เมื่อเราต้องการทดลองหลายๆ โมเดลพร้อมกันครับ

    และสุดท้าย แบ่งข้อมูลเป็นสัดส่วน

    X_trained, X_test, y_trained, y_test = train_test_split(df_X_scaled, y, test_size = 0.2, train_size=0.8, random_state = 42)
    

    ในอนาคตเราจะมีเจาะลึกวิธีการแบ่งข้อมูลด้วยเทคนิคอื่นๆกันครับ ตอนนี้เราใช้ Train Test Split ไปก่อน

    Train Models

    models = {
            'Logistic Regression': LogisticRegression(random_state=42, max_iter=1000),
            'Decision Tree': DecisionTreeClassifier(random_state=42),
            'Random Forest': RandomForestClassifier(random_state=42),
            'K-Nearest Neighbors': KNeighborsClassifier(),
            'Support Vector Machine': SVC(random_state=42, probability=True)
        }
        for model_name, model in models.items():
            model.fit(X_trained, y_trained)
    

    อย่าลืมใส่ probability=True เพื่อให้เราสามารถอ่านค่า AUC ได้

    Score

    เมื่อเรา Train เสร็จแล้วก็ถึงเวลาวัดผลแล้วละ

    def evaluation_model(model_name, model,y_test,X_test):
        y_pred = model.predict(X_test)
        cm = confusion_matrix(y_test, y_pred)
        print(f'Model : {model_name}')
        print(cm)
        print(f'Accuracy Score: {accuracy_score(y_test, y_pred)}')
        print(f'Precision Score: {precision_score(y_test, y_pred)}')
        print(f'Recall Score: {recall_score(y_test,y_pred)}')
        print(f'F1 Score: {f1_score(y_test, y_pred)}')
        print(classification_report(y_test,y_pred,target_names=['NO CHD (0)', 'CHF (1)']))
        if hasattr(model, 'predict_proba'):
            y_pred_proba = model.predict_proba(X_test)[:, 1] # Probability of the positive class (1)
            roc_auc = roc_auc_score(y_test, y_pred_proba)
            print(f"ROC AUC Score: {roc_auc:.4f}")
        else:
            print("ROC AUC Score: Not available (model does not provide probabilities directly).")
        print(f"{'='*50}\n")
    

    อะอะ ก่อนไปดูผลลัพธ์เรามาทำความรู้จัก Metrics ต่างกันก่อนดีกว่า

    เริ่มกันที่ตัวแรกเลย Accuracy

    Accuracy

    Accuracy คือ metric ที่ใช้งานง่ายที่สุด ตามชื่่เลยครับว่า ความแม่นยำ บอกว่าโมเดลเราทำนายถูกทั้งหมดกี่ % จากตาราง Confusion Matrix เราสามารถคำนวณได้ด้วยสูตรข้างล่างนี้

    ถ้าแบบทางการหน่อย เราจะเขียนสูตรว่า accuracy = (TP + TN)/ N โดยค่า accuracy จะมีค่าอยู่ระหว่าง 0-1 ยิ่งเข้าใกล้ 1 แปลว่าโมเดลเราทำนายผลได้ดีมาก

    Precision

    จริงๆ อันนี้ก็แปลว่าความแม่นยำ เหมือนกัน แต่จะมีความหมายคนละแบบ นิยามของ Precision คือความน่าจะเป็นที่โมเดลทำนาย CHD คิดเป็นจากการทำนายถูกทั้งหมดกี่ % หรือก็คือ ถ้าชี้หน้าว่า คนๆนี้ป่วย โอกาสที่ป่วยก็คือ Precision% นั้นเอง

    แทนค่าในสมการ precision = TP / (TP + FP)

    Recall

    นิยามของ Recall คือความน่าจะเป็นที่โมเดลสามารถตรวจจับ คนเป็น CHD จากจำนวน CHD ทั้งหมดใน ผลรวมคอลั่มแรกของ confusion matrix) สมมติในโรงพยาบาลมีคนไข้ 100 คน ถ้าตรวจพบแค่ 70 ก็คือมี ค่า Recal ที่ 70%

    แทนค่าในสมการ recall = TP / (TP + FN)

    F1-Score

    F1-Score คือค่าเฉลี่ยแบบ harmonic mean ระหว่าง precision และ recall นักวิจัยสร้าง F1 ขึ้นมาเพื่อเป็น single metric ที่วัดความสามารถของโมเดล (ไม่ต้องเลือกระหว่าง precision, recall เพราะเฉลี่ยให้แล้ว) เพราะตามความเป็นจริง Precision และ Recall จะสวนทางกัน เขาก็เลยคิด F1-Score เพื่อปิดจุดอ่อนนี้

    • การดูแต่ค่า Accuracy อย่างเดียวมีโอกาสอธิบายการทำนายผิดพลาดได้
    • ในทางปฏิบัติเราจะดูค่า precision, recall, F1 ร่วมกับ accuracy เสมอ โดยเฉพาะอย่างยิ่งเวลาเจอกับปัญหา imbalanced classification i.e. y {0,1} มีสัดส่วนไม่เท่ากับ 50:50

    AUC

    AUC ย่อมาจาก “Area Under Curve” AUC มีค่าอยู่ระหว่าง 0-1 ยิ่งเข้าใกล้ 1 แปลว่าโมเดลทำนาย Y ได้ดี เป็นอีก 1 ใน metrics สำคัญมากๆในงาน Datascience

    ด้านล่างคือผลลัพธ์ที่ได้ของแต่ละโมเดล

    เอาละหลังจากรู้จัก Metrics ต่างๆละเรามาดูผลลัพธ์กันเล๊ยยย~~~

    Model : Logistic Regression
    [[605   5]
     [117   5]]
    Accuracy Score: 0.8333333333333334
    Precision Score: 0.5
    Recall Score: 0.040983606557377046
    F1 Score: 0.07575757575757576
                  precision    recall  f1-score   support
    
      NO CHD (0)       0.84      0.99      0.91       610
         CHF (1)       0.50      0.04      0.08       122
    
        accuracy                           0.83       732
       macro avg       0.67      0.52      0.49       732
    weighted avg       0.78      0.83      0.77       732
    
    ROC AUC Score: 0.7078
    ==================================================
    

    Model : Decision Tree
    [[528  82]
     [ 89  33]]
    Accuracy Score: 0.7663934426229508
    Precision Score: 0.28695652173913044
    Recall Score: 0.27049180327868855
    F1 Score: 0.27848101265822783
                  precision    recall  f1-score   support
    
      NO CHD (0)       0.86      0.87      0.86       610
         CHF (1)       0.29      0.27      0.28       122
    
        accuracy                           0.77       732
       macro avg       0.57      0.57      0.57       732
    weighted avg       0.76      0.77      0.76       732
    
    ROC AUC Score: 0.5680
    ==================================================
    

    Model : Random Forest
    [[600  10]
     [110  12]]
    Accuracy Score: 0.8360655737704918
    Precision Score: 0.5454545454545454
    Recall Score: 0.09836065573770492
    F1 Score: 0.16666666666666666
                  precision    recall  f1-score   support
    
      NO CHD (0)       0.85      0.98      0.91       610
         CHF (1)       0.55      0.10      0.17       122
    
        accuracy                           0.84       732
       macro avg       0.70      0.54      0.54       732
    weighted avg       0.80      0.84      0.79       732
    
    ROC AUC Score: 0.6859
    ==================================================
    

    Model : K-Nearest Neighbors
    [[588  22]
     [104  18]]
    Accuracy Score: 0.8278688524590164
    Precision Score: 0.45
    Recall Score: 0.14754098360655737
    F1 Score: 0.2222222222222222
                  precision    recall  f1-score   support
    
      NO CHD (0)       0.85      0.96      0.90       610
         CHF (1)       0.45      0.15      0.22       122
    
        accuracy                           0.83       732
       macro avg       0.65      0.56      0.56       732
    weighted avg       0.78      0.83      0.79       732
    
    ROC AUC Score: 0.6241
    ==================================================
    
    

    Model : Support Vector Machine
    [[610   0]
     [119   3]]
    Accuracy Score: 0.837431693989071
    Precision Score: 1.0
    Recall Score: 0.02459016393442623
    F1 Score: 0.048
                  precision    recall  f1-score   support
    
      NO CHD (0)       0.84      1.00      0.91       610
         CHF (1)       1.00      0.02      0.05       122
    
        accuracy                           0.84       732
       macro avg       0.92      0.51      0.48       732
    weighted avg       0.86      0.84      0.77       732
    
    ROC AUC Score: 0.6525
    ==================================================
    

    หลังจากที่เราได้ผลลัพธ์ของแต่ละโมเดลมาเรียบร้อยแล้ว ก่อนที่จะไปประเมินผลลัพธ์ของแต่ละโมเดลกัน เรามาทำความเข้าใจกันก่อนว่า ทำไมผมถึงทักว่าเราจะกลับมาดูตรง value_count กันครับ

    Imbalanced Dataset

    หากเราลองดูค่า Relative Frequency ของ dataset นี้

    TenYearCHD
    0    0.847648
    1    0.152352
    

    คุณเห็นอะไรไหมครับ ใช่ ข้อมูลของคนที่ไม่ได้ป่วยเป็นโรคหลอดเลือดหัวใจ คิดเป็น 85% ของข้อมูลนี้ นั้นแปลว่าถ้าผมแค่นั่งเดา 100 ครั้งว่า คนนี้ไม่เป็นโรคหลอดเลือดหัวใจ ผมก็เดาถูกตั้ง 85% เลยนะครับ

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

    และในกรณีนี้เมื่อมี Class หนึ่งที่เยอะผิดปกติ โมเดลก็มีแนวโน้มที่จะเรียนรู้และเดาข้อมูลที่มีเยอะกว่าได้แม่นยำกว่าส่งผลให้โอกาสที่จะทายข้อมูลอีกด้านนั้นลดลง

    อย่างในกรณีนี้คุณลองคิดดูสิว่าถ้าโมเดลทำนายว่าคนนี้ไม่ได้เป็นหรอกโรคหลอดเลือดหัวใจ แต่จริงๆแล้วเขาเป็น มันจะอันตรายถึงชีวิตเขาขนาดไหน เขาอาจะถึงขั้นเสียชีวิตเลยก็ได้นะครับ

    ในบทความถัดๆไป เราจะมีดูกันว่าจะมีวิธีการแก้ปัญหาอย่างไรในกรณีที่ข้อมูลไม่เท่ากันแบบนี้อีก เพื่อลดความผิดพลาดของโมเดลในการทำนายผลกันนะครับ

    Evaluation

    เรามาเริ่มกันที่ Logistic Regression กันก่อน

    โมเดล ClassificationAccuracy (ความแม่นยำรวม)Precision (Class 1)Recall (Class 1)F1-Score (Class 1)ROC AUC Score
    Logistic Regression0.8360.50.0410.0760.7078
    Decision Tree0.7660.2870.270.2780.568
    Random Forest0.8360.5450.0980.1670.6859
    K-Nearest Neighbors0.8280.450.1480.2220.6241
    Support Vector Machine (SVM)0.83710.0250.0480.6525

    จากตารางข้างต้นจะเห็นได้ว่า Model ที่มีค่า ROC AUC มากที่สุดนั้นคือ Logistic Regression รองลงมาเป้น Random Forest นั้นแปลว่าสามารถจำแนกผู้ป่วยกับไม่ป่วยได้ค่อนข้างดีในหลากหลายสถานการณ์ สำหรับ Logistic Regresssion

    อย่างไรก็ตามด้วยค่า Recall ที่แปลว่าคุณเดาถูกกี่ครั้งจากคนที่ป่วยทั้งหมด ดันได้คะแนนสำหรับ Class 1 แค่ 0.04 นั้นแปลว่าถ้ามี 100 คน จะบอกว่าป่วยได้แค 4 คนซึ่งนี้มันต่ำมากๆ

    แม้ว่า Decision Tree จะมีค่า Recall สูงที่สุดใน 5 โมเดล แต่ก็มีค่า Precision ที่ต่ำที่สุดในกลุ่มเช่นกันก็คือถ้าทายว่าป่วยไม่ป่วยดันทายถูกแค่ 28% เท่านั้นอง

    สำหรับ Random Forest นั้นแม้ว่าค่า Accuracy กับ ค่า ROC AUC จะทำได้ดีแต่ดันไปตกม้าตายที่ค่า Recall ที่ได้แค่ 0.1 เหมือนจะดีกว่า Logistic แต่ก็ยังทำผลงานได้แย่อยู่ดี เกินกว่าจะนำออกไปใช้จริงได้

    ในส่วนของ KNN โดยภาพรวมแล้วไม่ได้หวือหวา หรือเด่นไปกว่าโมเดลไหนเลยครับ

    และสุดท้าย SVM ที่มีคะแนน Precision = 1 เลยทีเดียวแปลว่าเดาว่าเป็นกี่ครั้งก็คือถูกหมดแต่ว่า ดันมีค่า Recall แค่ 0.025 ก็คือ ถ้ามี 100 คนมันจะเดาถูกแค่ 3 คน แต่ทุกครั้งที่ทำนายคือถูกชัวร์ แต่มันก็ต่ำเกินไปแปลว่ามันมีโอกาสข้ามคนป่วยถึง 97 คนเลยทีเดียว

    Conclusion

    สรุปแล้วในภาพรวมเมื่อสรุปและประเมินผลที่ได้โมเดลที่น่าจะทำผลงานได้ดีที่สุดหากมีการนำไปใช้จริงก็จะเป็น Logistic Regression ด้วยคะแนน ROC AUC ที่มากที่สุดนั้นเอง อย่างไรก็ตามด้วยค่า AUC แค่ 0.7 หรือ 70% นั้นถือว่ายังไม่ดีพอ (ถ้าดีควรจะมีค่ามากกว่า 80%) ซึ่งเป็นปัญหาทีเกิดจากการที่ข้อมูลที่เรามีนั้นไม่ได้สมดุลระหว่างผู้ป่วยที่เป็นโรคหัวใจ หรือไม่เป็น

    ด้วยผลดังกล่าว อาจส่งผลให้โมเดลของเราดูมีค่าความแม่นยำ (Accuracy) ที่สูงแต่กลับไม่สามารถนำไปใช้ประโยชน์ได้จริงหากหน้างานต้องการความละเอียดอ่อน ลองคิดดูสิครับถ้าเราเอาโมเดลนี้ไปใช้ แล้วมันดันข้ามคนป่วยจริงๆไป ผมว่ามันเป็นอะไรที่เรารับไม่ได้แน่ๆครับ

    ดังนั้น ในบทความข้างหน้า หลังจากที่เขียน Main Serie หมดแล้ว ผู้เขียนจะมานำเสนอวิธีการและประยุกต์ใช้เทคนิคต่างๆเพื่อจัดการกับปัญหาเหล่านี้

    เพื่อให้โมเดลของเราไม่เพียงแค่ทำนายได้แม่นยำในภาพรวม แต่ยังสามารถตรวจจับ Class สำคัญได้อย่างมีประสิทธิภาพมากยิ่งขึ้น!”

    References

    1. Random Forest
    2. เจาะลึก Random Forest !!!— Part 2 of “รู้จัก Decision Tree, Random Forest, และ​ XGBoost!!!”
    3. ข้อดี-ข้อเสีย ของแต่ละ Machine Learning Algorithm
    4. Scikit-Learn Official Website

  • Main Series: Part 1 ทำความเข้าใจหลักการ Linear Regressionใน Machine Learning

    Main Series: Part 1 ทำความเข้าใจหลักการ Linear Regressionใน Machine Learning

    เป็นเวลาหลายเดือนที่ผมเรียนจบจาก Data Science Bootcamp กับ Ad’ Toy และได้เริ่มเขียน Posts หลายๆอัน หลังจากมัวไต่แรงก์ TFT Set 14 ผมว่ามันถึงเวลาอันสมควรแล้วที่จะเขียน Post เพื่อทบทวนและแชร์ไอเดียเกี่ยวกับ Classical Machine Learning ที่ผมได้เรียนในคอร์สกับ Ad’ Toy

    แต่ครั่นจะให้ผมแค่อธิบายสิ่งที่ได้เรียนมาในคอร์สก็ดูจะเบสิคเกินไป ผมจะพาทุกท่านไปดูการทำและวิเคราะห์ไอเดียผ่าน data ใน kaggle แทนเพื่อให้ทุกท่านได้เห็นภาพมากขึ้น

    เนื้อหาส่วนนี้จะแบ่งออกเป็น 4 พาร์ทเพื่อไม่ให้ยาวเกินไปนะครับ

    โดยในส่วนของ Main Series นั้นเราจะโฟกัสไปที่การแตะๆ Walkthrough เนื้อหาภาพรวมกันเสียก่อน แล้วเดี๋ยวเราค่อยมาเจาะลึกกันสำหรับแต่ละ Algorithms ใน Series ถัด

    Table of Content


    Introduction: Why Classical Machine Learning Matters ?

    ในยุคที่ปัญญาประดิษฐ์ ( AI ) กลายเป็นส่วนหนึ่งของการดำรงชีวิต ทั้งในด้านการเพิ่มประสิทธิภาพและคุณภาพของงาน ไปจนถึงการทดแทนแรงงานมนุษย์ในหลายภาคส่วน

    ความเข้าใจในรากฐานของเทคโนโลยีเหล่านี้ โดยเฉพาะ Machine Learning (ML) จึงเป็นสิ่งสำคัญอย่างมาก

    การเรียนรู้ Classical Machine Learning ไม่เพียงช่วยให้เรามองเห็นเบื้องหลังการทำงานของระบบอัจฉริยะ แต่ยังเป็นจุดเริ่มต้นที่แข็งแกร่งในการต่อยอดสู่เทคโนโลยีสมัยใหม่อย่าง Deep Learning และ Generative AI เช่นกัน

    แล้ว Machine Learning คืออะไร? โดยพื้นฐานแล้ว Machine Learning คือศาสตร์แขนงหนึ่งของวิทยาการคอมพิวเตอร์ที่เกี่ยวข้องกับการทำให้คอมพิวเตอร์สามารถเรียนรู้จากข้อมูลได้เอง

    โดยไม่จำเป็นต้องถูกโปรแกรมให้ทำงานแบบชัดเจนในทุกขั้นตอน เมื่อทำการสอนเสร็จแล้ว เราก็จะได้ Model ในรูปแบบสมการ หรือไม่เป็นรูปแบบสมการที่ใช้ในการทำนาย การจัดกลุ่มและอื่นๆได้อีกเช่นกัน

    Machine Learning

    สำหรับ Machine Learning แล้วก็จะแบ่งเป็น Classic และ Modern โดยเราจะโฟกัสไปที่ Classic กันก่อน

    สำหรับ Classical Machine Learning นั้นสามารถแบ่งเป็น 2 กลุ่มใหญ่ๆได้แก่

    1. Supervised Learning 
    2. Unsupervised Learning
    3. Reinforcement Learning

    Supervised Learning 

    เป็นวิธีการสอนรูปแบบหนึ่งที่เราจะให้คอมพิวเตอร์เรียนรู้จากข้อมูลที่มีหัวตาราง (labeled data) โดยประกอบไปด้วย 2 รูปแบบได้แก่

    1. Regression
    2. Classification

    Unsupervised Learning

    อีกหนึ่งวิธีการสอนที่จะให้ data แบบไม่มี metadata เพื่อหารูปแบบ หรือสรุปข้อมูลออกมา โดยรุปแบบการเรียนรู้นี้จะนิยมใช้เทคนิค Clustering และ Principal Component Analysis หรือ PCA 

    Reinforcement Learning

    รุปแบการสอนที่เน้นให้ Agent หรือตัวโมเดลสำรวจ สภาพแวดล้อม environment และเลือกกระทำการใดๆต่าง Action โดยที่การกระทำเหล่านั้นจะมีผลลัพธ์ตามมาไม่ว่าจะเป็ฯ รางวัล Reward หรือ บทลงโทษ Penalty

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

    ในบทความนี้ผมจะพูดถึงแค่ 1 ใน รูปแบบของ Supervised Learning นั้นคือการทำ Linear Regression เสียก่อน เพื่อไม่ให้ยาวจนเกินไปนะครับ เกริ่นกันมามากพอละ ป่ะลุยกั๊นนนนน

    Linear Regression

    ก่อนที่เราจะไปพูดถึง Linear Regression นั้นเราควรเข้าใจกับคำว่า สหสัมพันธ์ (Correlation) เสียก่อน  

    Correlation เป็นการวัดความสัมพันธ์เชิงเส้นตรงระหว่างตัวแปรสองตัว โดยจะมีค่าอยู่ระหว่าง [-1,1] เท่านั้น

    Correlation บอกได้แค่ตัวแปรสองตัวนี้มีความสัมพันธ์กันระดับไหน วัดออกมาเป็นตัวเลขระหว่าง [-1, +1] เครื่องหมายบวกลบบอกทิศทางความสัมพันธ์ของตัวแปร ถ้าไม่มีความสัมพันธ์เลย correlation จะเท่ากับศูนย์ (หรือประมาณ +/- 0.1)

    Linear Regression เป็นสมการ หรือโมเดลรูปแบบนึงที่ใช้ประเมินคุณภาพของความสัมพันธ์ (relationship) โดยมักจะมาในรูปแบบ สมการเส้นต้น หรือ สมการหลายตัวแปร

    y = mx + b
    

    หรือ

    y = b0 + b1*x1 + b2*x2 + b3*x3 + .. + bk*xk  
    

    โดย algorithm นี้จะเป็น Alogirthm ที่เหมาะสมก็ต่อเมื่อง error ที่เกิดขึ้นรอบๆ เส้นตรงดังกล่าวต่ำนั้นเอง

    Linear Regression เหมือนเป็น add-on ต่อยอดจากการหาค่า Correlation ว่าถ้าตัวแปร x เปลี่ยนเท่านี้ตัวแปร y จะ เปลี่ยนไปเท่าไหร่นั้นเอง

    ตัว m ในสมการเราจะเรียกว่า Regression Coefficient และ b คือ Interception point

    Dataset & How We Preprocess Data

    สำหรับ ตัวอย่างนี้ผมจะใช้ dataset ที่ชื่อว่า AirQualityUCI นะครับสามารถคลิกลิ้งค์ตรงนี้เพื่อเข้าไปหาต้นทางได้เลย

    ก่อนโหลด dataset เราก็ต้องทำความเข้าใจแต่ละ Columns ก่อนว่าคืออะไรกันบ้างครับ

    FeatureDescription
    Dateวันที่วัดค่า
    Timeเวลาที่วัดค่า
    CO(GT)ความเข้มข้นของ Carbon Monoxide ในหน่วย (µg/m³).
    PT08.S1(CO)ค่าที่ได้จากการวัด Carbon Monoxide จากเครื่องวัด
    NMHC(GT)ความเข้มข้นของ non-methane hydrocarbons (NMHC) (µg/m³).
    C6H6(GT)ความเข้มข้นของ benzene (C6H6) in the air (µg/m³).
    PT08.S2(NMHC)ค่าที่ได้จากการวัด non-methane hydrocarbons (NMHC) จากเครื่องวัด
    NOx(GT)ความเข้มข้นของ nitrogen oxides (NOx) in the air (µg/m³).
    PT08.S3(NOx)ค่าที่ได้จากการวัด NOx จากเครื่องวัด
    NO2(GT)ความเข้มข้นของ nitrogen dioxide (NO2) in the air (µg/m³).
    df=pd.read_csv(filepath_or_buffer='AirQualityUCI.csv', sep=',', decimal=',', header=0)
    

    เนื่องจากไฟล์ต้นทางเป็นไฟล์ .csv เพราะฉะนั้นเราก็เรียกใช้ method .read_csv จาก library pandas เพื่อใช้อ่านโดยกำหนค่าตาม filepath (ให้ง่ายก็ลากไฟล์ที่จะอ่านมาไว้โฟลเดอร์เดียวกัน) และกำหนด separator (อักขระทีใช้ในการแบ่งข้อมูลใน csv ) และ decimal (อักขระที่ใช้แทนจุดทศนิยม) โดยกำหนดให้ header อยู่ที่แถว 0

    เมื่ออ่านค่าได้แล้วเราก็มาดูข้อมูลคร่าวๆกันเถอะ

    ⁠df.head() และ df.describe()

    df.head()
    df.describe()

    จากตัวอย่างหัวตารางจะเห็นได้ว่าข้อมูลนี้จะเกี่ยวกับการวัดคุณภาพอากาศที่เก็บได้เป็นระยะเวลา 1 ปี 1 เดือนนะครับ โดยมีวัตถุประสงค์เพื่อใช้ในการทำ model สำหรับการทำนายและวิเคราะห์ข้อมูลภายในงานด้าน วิทยาศาสตร์สิ่งแวดล้อมและสุขภาพอนามัย

    ในข้อมูลจะเห็นว่ามี 2 Columns ที่ไม่มีชื่อใช่ไหมครับ เพราะฉะนั้นเราก็ต้อง Drop 2 Columns นั้นออกด้วยคำสั่งด้านล่าง

    df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
    

    คำสั่ง loc ใน pandas หมายถึงการเข้าถึงข้อมูลใน DataFrame โดยอาศัยการกำหนด rows หรือ columns หรือจะใช้ค่า boolean arrays ก็ได้เช่นกัน

    จากคำอธิบาย Dataset ใน Kaggle ได้บอกว่าบางค่าในแต่ละเดือนก็ไม่สามารถบันทึกได้เขาก็เลยใส่ -200 มาเพราะฉะนั้นเราก็ต้องลบออกเช่นกัน

    df = df.replace(-200,np.nan).dropna()
    

    ก็คือผมเปลี่ยน -200 ทั้งหมดให้เป็น np.nan ก่อนแล้วก็ค่อย drop NaN ทิ้ง

    โอเคเราได้ทำการ clean data เรียบร้อยแล้ว ในขั้นตอนถัดไปเราก็สามาถนำข้อมูลนี้ไปใช้ train ได้แล้วครับ

    Simple Linear Regression

    จากข้อมูลผมจะทำการเริ่มจากใช้ ตัวแปรตัวเดียวเพื่อหาความสัมพันธ์ของข้อมูลก่อน ในกรณีนี้จะเลือก y เป็น NMHC และ x เป็น C6H6(GT)

    y = np.array(df['NMHC(GT)']).reshape(-1,1)
    

    และ

    X = np.array(df['C6H6(GT)']).reshape(-1,1)
    

    Split Data

    เมื่อได้ X และ y เป็นที่เรียบร้อยแล้วเราก็นำข้อมูลดังกล่าวมา split โดยผลแบ่งเป็น ใช้ฝึก 80% และ ใช้ทดสอบ 20% และสาเหตุที่ใส่ค่า random_state = 42 เพราะว่าจะให้ได้ผลลัพธ์การสุ่มแบ่งข้อมูลเหมือนเดิมในทุกๆครั้ง

    X_trained, X_test, y_trained, y_test = train_test_split(X, y,train_size=0.8, test_size=0.2, random_state=42)

    Train and Validate

    หลังจากนั้นเราก็จะนำ data ที่แบ่งมา train กัน

    lnr = LinearRegression()
    lnr.fit(X_trained, y_trained)
    y_pred = lnr.predict(X_test)
    
    1. สร้างฟังก์ชั่น linear regression ด้วยฟังก์ชั่น LinearRegression
    2. นำ data X_trained และ y_trained มา train
    3. ทำนายค่า X_test ด้วย predict()

    จากนั้นก็ทำการหาค่า metrics ต่างๆ

    print("Mean Absolute Error Trained ", mean_absolute_error(y_trained, y_trained_pred))
    print("Mean Absolute Error Test ", mean_absolute_error(y_test, y_pred))
    

    จะได้ผลลัพธ์ดังนี้

    # Mean Absolute Error Trained  61.831639475521364
    # Mean Absolute Error Test  60.9002976731785

    หน่วยที่ใช้วัดจะเป็นหน่วย µg/m³ ไมโครกรัม ต่อ ลูกบาศก์เมตร หากดูจากค่า trained MAE และ test MAE นั้นถือว่ามีค่าใกล้กันแต่แปลว่า model เราค่อนข้าง generalised

    อย่างไรก็ตาม MAE เป็นค่าที่ยิ่งใกล้ 0 จะยิ่งให้ผลลัพธ์ที่ดีเพราะแปลว่าเราอ่านค่าคลาดเคลื่อนน้อย แต่นี้ MAE ตั้ง 60 แปลว่า simple linear regression model ไม่ตอบโจทย์กับ dataset นี้

    เพราะฉะนั้นเรามาลองหาค่าใหม่แต่ใช้เป็น multi-varaible linear regression กันอีกรอบ

    Multi-variable Linear Regression

      y = np.array(df['NMHC(GT)']).reshape(-1,1)
        df_pollutant = df[['PT08.S2(NMHC)', 'C6H6(GT)','T', 'RH', 'AH', 'NO2(GT)', 'NOx(GT)', 'CO(GT)']]
        X = np.array(df_pollutant).reshape(-1, len(df_pollutant.columns))
        X_trained, X_test, y_trained, y_test = train_test_split(X, y,train_size=0.8, test_size=0.2, random_state=42)
        lnr = LinearRegression()
        lnr.fit(X_trained, y_trained)
        y_pred = lnr.predict(X_test)
        y_trained_pred = lnr.predict(X_trained)
        print("Mean Absolute Error Trained ", mean_absolute_error(y_trained, y_trained_pred))
        print("Mean Absolute Error Test ", mean_absolute_error(y_test, y_pred))
    

    เราก็ได้ผลลัพธ์ดังนี้

    # Mean Absolute Error Trained  59.31405073871236
    # Mean Absolute Error Test  57.858497022746086

    สังเกตว่า แม้เราจะเพิ่มตัวแปรเข้าไปเป็นจำนวนมากแต่ค่า MAE กับลดลงเพียง 2-3 µg/m³ เท่านั้นเอง ซึ่งก็ยังคลาดเคลื่อนเยอะมาก

    ลองจินตนาการว่าค่าที่แท้จริงได้ 7 µg/m³ แต่ดันอ่านได้ 64-66 µg/m³ ดูสิครับทำนายกันผิดผลาดแน่นอน

    No Free Lunch

    จากตัวอย่างข้างต้นทำให้เราได้ข้อสรุปว่าการที่เราจะ train model อะไรซักอย่างนั้นจะต้องลองหลายๆ model เพื่อหา model ที่ดีที่สุดทั้งในเรื่องของเวลาและค่าใช้จ่าย

    ซึ่งนี้เป็นไปตามหลักการที่ว่า ไม่มีโมเดลไหนเก่งที่สุด และสามารถตอบโจทย์ได้ทุกปัญหา หรือ No Free Lunch บางทีอาจจะมีโมเดลอื่นอย่าง Decision Tree, Random Forest และ อื่นๆ ที่จะสามารถนำ data นี้ไปทำนายได้อย่างมีประสิทธิภาพมากขึ้นไปอีกได้เช่นกัน

    Conclusion & Evaluation

    สรุป dataset นี้ไม่เหมาะที่จะใช้ Linear Regression model ในการทำนายค่าเนื่องจากว่ามีผลลัพธ์ที่ไม่น่าพึ่งพอใจและเสี่ยงตอนความคลาดเคลื่อนสูง

    แม้ว่าจะมีกาารเพิ่มตัวแปรเข้าไปแล้วก็ตาม ทั้งนี้ dataset นี้อาจเหมาะกับโมเดลอื่นๆมากกว่า

    จากที่ ผมลองดูมา dataset ใน kaggle ที่ดูแล้วน่าจะใช้ Linear Regression ได้ก็น่าจะเป็น California Housing Price ครับเพื่อนๆก็สามารถลอง ขั้นตอน cleansing data (preprocessing) บวกกับ การสร้าง Linear Regression Model ที่ผมเขียนไปลองดูกันได้ ลองแล้วได้อย่างไรก็อย่าลืมมาแชร์กันนะครับ

    Scatter Plot ของ y_pred, X_test

    แล้วเพื่อนๆ เคยเจอโปรจคไหนที่มีปัญหา No Free Lunch อย่างนี้ไหมครับ

    Sourcecode

    https://gist.github.com/patrawi/4af831586d73e492362cb2a19148f79d.js

    References

    # ทำความรู้จัก “Linear Regression” Algorithm ที่คนทำ Machine Learning ยังไงก็ต้องได้ใช้!
    # ทำนายราคาบ้าน Boston ด้วย Linear Regression

  • สรุป key insight จาก Smoking in UK dataset การบ้าน Ad’toy

    สรุป key insight จาก Smoking in UK dataset การบ้าน Ad’toy

    การวิเคราะห์พฤติกรรมการสูบบุหรี่ในสหราชอาณาจักร

    สวัสดีครับ หลังจากห่างหายไปเป็นอาทิตย์ วันนี้เรากลับมากับ coursework จาก คอร์ส Data Analyst Bootcamp ของ Ad’ Toy โดยการบ้านนี้จะเป็นส่วนหนึ่งของวิชา Spreadsheet โฟกัสที่การทำ pivot table และ dashboard

    สำหรับการบ้านนี้ผมเลือกที่จะใช้ dataset จากบน kaggle แหล่งรวม free dataset จากทั่วทุกมุมโลก ซึ่งในคราวนี้มันจะเกี่ยวกับ การสูบบุหรี่ภายในสหราชอาณาจักร

    สาเหตุที่ผมเลือกหัวข้อนี้เพราะว่าปีหน้าผมกำลังจะไปเรียนต่อ ป.โท เลยคิดว่าอยากทำอะไรที่เกี่ยวกับประเทศที่เราไปดีกว่า

    ในสหราชอาณาจักรอังกฤษตอนนี้กำลังจะบัญญัติกฎหมายใหม่ว่าด้วยการซื้อบุหรี่นั้นมีความผิดหากผู้ซื้อมีอายุต่ำกว่า 15 ปี แม้ว่าในปัจจุบันจำนวนผู้สูบบุหรี่ในช่วงวัยรุ่น นั้นจะลดลงแต่จำนวนผู้ที่เลือกหันไปสูบ บุหรี่ไฟฟ้า ( Vape ) ก็มีเพิ่มขึ้นเช่นกัน แม้งานวิจัยหลายอย่างจะชี้ตรงกันว่า การสูบบุหรี่ไฟฟ้ามีผลข้างเคียงน้อยกว่าบุหรี่ธรรมดา แต่ในระยะยาวแล้ว ก็สามารถส่งผลต่อระบบการหายใจ สมองและหัวใจได้เช่นเดียวกัน

    จากการสำรวจข้อมูลของ ทาง Office of National Statistics ใน สหราชอาณาจักรอังกฤษ พบว่า 50% เลิกบุหรี่เพราะว่าปัญหาด้านสุขภาพในขณะที่ 25% เลิกเพราะราคาที่ปรับตัวสูงขึ้นของบุหรี่

    หลังจากที่เรารู้ข้อมูลบางส่วนเกี่ยวกับพฤติกรรมการสูบบุหรี่ไปเรียบร้อยแล้ว งั้นเรามาดู columns ของ dataset นี้กันก่อนดีกว่าครับ

    Table of Content

    About Dataset

    แบบสำรวจนี้เป็นการสำรวจข้อมูลพฤติกรรมการสูบบุหรี่ในสหราชอาณาจักร โดยข้อมูลดังกล่าวสามารถนำมาใช้วิเคราะห์ลักษณะของผู้สูบและประเภทของบุหรี่ที่สูบ โดยข้อมูลนี้จะเก็บจากกลุ่มตัวอย่าง (sample) 1,691 คน ด้วยตัวแปรทั้ง 12 ตัวดังนี้

    ColumnDescription
    genderชาย หรือ หญิง
    ageอายุ
    marital_statusสถานะการแต่งงาน แบ่งเป็น หย่าร้าง แต่ง แยกกันอยู่ โสด และ หม่าย
    highest_qualificationระดับการศึกษา
    nationalityสัญชาติ
    ethnicityเชื้อชาติ
    gross_incomeรายได้สุทธิ
    regionพื้นที่
    smokeสถานะว่าสูบหรือไม่สูบ
    amt_weekendsจำนวนบุหรี่ที่สูบ ต่อวันในช่วงวันหยุดสุดสัปดาห์
    amt_weekdaysจำนวนบุหรี่ที่สูบ ต่อวันในช่วงวันธรรมดา
    typeประเภทบุหรี่ที่สูบ

    How can I create a chart in with pivot table

    1. ขั้นตอนแรก เลือกข้อมูลทั้งหมดโดยคลิกที่บริเวณมุมซ้ายบนของตาราง
    an image show an area for drag all of content in a sheet

    2. หลังจากเลือกข้อมูลทั้งหมดแล้ว ให้เลือกเมนู Insert > Pivot Table ระบบจะถามว่าต้องการสร้างใน Sheet ใหม่หรือใช้ Sheet ที่มีอยู่แล้ว

    A dialog show asking whether you will create in a new sheet or not

    3. เมื่อกด Create จะได้ตารางว่างและ Pivot Table Editor

    A blank table
    A pivot table editor

    4. เมื่อได้หน้าตาแบบนี้แล้ว เราสามารถลากหรือกดปุ่ม Add เพื่อเพิ่มข้อมูลในส่วนต่างๆ เช่น:

    • Filters
    • Rows
    • Columns
    • Values

    5. เราสามารถเลือกให้แสดงค่าผลรวมโดยเลือกที่ Show Totals Checkbox

    6. เมื่อได้ตารางที่ต้องการแล้ว ให้เลือกตารางดังกล่าวแล้วไปที่เมนู Insert > Chart

    7. สามารถคลิกที่ปุ่มสามจุด (⋮) เพื่อ:

    • ปรับแต่งรายละเอียดอื่นๆ
    • แก้ไขประเภทของกราฟให้เหมาะสม
    • ปรับแต่งชื่อกราฟ
    • เปลี่ยนสัญลักษณ์
    Chart Editor

    Insight

    ภายในข้อมูลชุดนี้ ไม่มีการระบุข้อมูลประเภทเวลา (date/time) ทำให้เราไม่สามารถสร้างกราฟประเภท line chart หรือ time series chart ได้

    หาก กราฟนี้มีข้อมูลประเภทนี้เพิ่มขึ้นเราสามารถวัดได้ว่าในแต่ละช่วงเวลาของปี ปริมาณบุหรี่ที่สูบแปรผันหรือไม่ หรือ ถ้าข้อมูลเวลายาวนานเพียงพอก็สามารถนำมาตั้งสมมติฐานว่า ในอดีตจนถึงปัจจุบันบุหรี่แต่ละประเภทมีความนิยมต่างกันเช่นไร (multiple line graph in one chart) เป็นต้น

    1. อายุเฉลี่ยของคนที่มาตอบแบบสอบถามนี้ เฉลี่ยแล้วอยู่ที่ 49.84 ปี
    2. แบบสอบถามนี้แบ่งออกเป็น ผู้ชาย 726 คน และ ผู้หญิง 965 คน
    Gender bar chart
    Gender bar chart

    3. ภายในแบบสอบถามนี้มีผู้สูบบุหรี่ (Yes) คิดเป็น 24.90% และ ไม่สูบ ( No ) 75.10%

    A smokers vs non-smokers donut chart
    A smokers vs non-smokers donut chart
    Cigarette type Vs Gross Income Stacked bar chart
    Cigarette type Vs Gross Income Stacked bar chart

    4. จากการวิเคราะห์ข้อมูลระหว่างช่วงรายได้สุทธิและประเภทของบุหรี่ ทำให้ผมพบข้อมูลที่น่าสนใจ ดังนี้

    • แบบซอง (Packets)
      • ในทุกช่วงรายได้ ผู้คนเลือกที่จะซื้อบุหรี่แบบซองมากกว่าที่จะมวนเองหรือเลือกทั้งคู่ โดยมีสัดส่วนถึง 75% ในช่วงรายได้ 26,400 ไปจนถึง 36,400 และมากกว่านั้น โดยในช่วงมากกว่า 36,400 บาทนั้นไม่พบผู้ที่ซื้อบุหรี่มวนเองเลย
      • ในขณะที่ช่วงรายได้อื่นก็คิดเป็นถึง 70% ของผู้ตอบแบบสอบถามในแต่ละช่วง
    • แบบมวนเอง (Hand-rolled)
      • ในช่วงรายได้ 2,600 ถึง 5,200 มีผู้ซื้อบุหรี่มามวนเองคิดเป็น 25% ของกลุ่มตัวอย่างที่มีรายได้อยู่ในช่วงดังกล่าว
      • จำนวนจะลดลงตามช่วงรายได้ที่เพิ่มขึ้น
    • เลือกทั้งคู่
      • ในทุกช่วงรายได้มีคนเลือกที่จะทั้งซื้อและมวนเอง ยกเว้นในช่วง 28,600-36,400 ที่ไม่พบกลุ่มตัวอย่างใดเลย
      • แม้ว่าจะพบได้ในเกือบทุกช่วง แต่จำนวนเปอร์เซ็นต์ก็ถือว่าเป็นส่วนน้อยเมื่อเทียบกับผู้ที่เลือกอย่างใดอย่างหนึ่ง
    • ปัจจัยที่มีผลต่อการเลือกประเภทบุหรี่
      • ราคา: ราคาบุหรี่แบบซองนั้นมีราคามากกว่า ส่งผลให้ในช่วงรายได้ต่ำๆ นั้นคนจึงเลือกที่จะซื้อแบบมวนเองอย่างมีนัยสำคัญ
      • ความสะดวก: การเลือกซื้อบุหรี่แบบซองนั้นง่ายต่อการใช้งานเมื่อเทียบกับแบบมวนเองที่ต้องนำมาเตรียม และยังมีราคาเพิ่มเติมหากต้องใช้อุปกรณ์ในการมวน
    Multiple bar chart about Nationality vs Region
    Multiple bar chart about Nationality vs Region

    5. จากข้อมูล ผมได้นำกราฟสัญชาติมาเทียบกับพื้นที่ เพื่อดูว่าในแต่ละพื้นที่นั้น สัญชาติใดมีแนวโน้มในการสูบบุหรี่อย่างไรบ้าง

    • British
      • กลุ่มตัวอย่างที่นิยามตัวเองว่าเป็น British มีจำนวนการสูบบุหรี่กระจายตัวอยู่ในทุกพื้นที่
      • สามารถพบได้มากในบริเวณ Midland & East Anglia, The North และ South East มากกว่า 25 คนในแต่ละพื้นที่
    • English
      • กลุ่มที่นิยามตนว่าเป็น English นั้นพบได้ทั่วไปในทุกพื้นที่ แต่จะพบมากที่สุดที่ The North เป็นจำนวน 60 คน และประมาณ 58 คนบริเวณ Midland & East Anglia
      • พบได้น้อยในพื้นที่ Scotland และ Wales
    • Irish
      • ผู้ที่นิยามว่าตนเป็น Irish นั้นพบได้น้อยมากในชุดข้อมูลนี้ โดยพบได้ในทุกพื้นที่แต่อยู่ในสัดส่วนที่น้อย ยกเว้นใน Wales ที่ไม่พบคนกลุ่มนี้เลย
      • แต่ละพื้นที่จะพบเพียงแค่ 5 คน หรือน้อยกว่านั้น
    • Scottish
      • คนสกอตแลนด์สามารถพบได้มากที่สุดในพื้นที่ Scotland โดยมีมากกว่า 40 คนจากกลุ่มตัวอย่าง และเพียง 3-5 คนใน London และ The North
    • Welsh
      • คนเวลส์สามารถพบได้มากใน Wales และเพียง 2-3 คนในพื้นที่ The North และ South East
    • ข้อสังเกตเพิ่มเติม
      • จากกลุ่มตัวอย่างนี้ ไม่สามารถสรุปได้อย่างแน่ชัดว่าคนสัญชาติใดที่สูบบุหรี่มากที่สุด เนื่องจากพื้นที่ที่ใช้ในการสำรวจนั้นไม่ครอบคลุมทั่วทั้งสหราชอาณาจักร
      • กลุ่มตัวอย่างเพียง 1,691 คน ไม่สามารถสรุปได้ว่าสัญชาติในแต่ละพื้นที่ใดสูบบุหรี่มากกว่ากัน
      • จากการสังเกตจะพบว่ามีความสอดคล้องอย่างหนึ่งคือ หากเป็นคน Scottish ก็จะพบมากในพื้นที่ Scotland และ Welsh ก็พบมากใน Wales
    Gross Income vs Qualification
    Gross Income vs Qualification

    6. กราฟนี้จะเป็นการพูดถึงรายได้สุทธิเทียบกับระดับการศึกษา จากกราฟเราจะเห็นว่าในช่วงที่รายได้สูง กลุ่มประชากรที่จบระดับปริญญาตรีขึ้นไปก็เยอะเช่นกัน คิดเป็นเกือบ 50% ของช่วงรายได้ 28,600 ถึง 36,400 และช่วงที่มากกว่า 36,400 ในขณะเดียวกัน ในช่วงรายได้ต่ำจะสังเกตได้ว่าระดับการศึกษานั้นมีความหลากหลายเป็นอย่างมาก อย่างไรก็ตามเรายังไม่สามารถบอกได้ว่ารายได้กับจำนวนคนที่สูบบุหรี่มีความสัมพันธ์กัน

    • Degree
      • จากกราฟจะพบว่าคนที่เรียนจบระดับอุดมศึกษาและสูบบุหรี่จะพบมากที่สุด คิดเป็นเกือบ 50% ในช่วงรายได้ 28,600-36,400 หรือมากกว่านั้น และไม่พบเลยในช่วง 2,600-5,200
      • ในส่วนของช่วงรายได้อื่นจะคิดเป็นเปอร์เซ็นต์ไม่เกิน 10%
    • GCSE/O Level
      • สามารถพบได้ในทุกช่วงรายได้ และคิดเป็น 10-35% ของคนสูบบุหรี่ในแต่ละช่วง
      • ไม่พบในช่วงรายได้ 28,600 – 36,400
    • No Qualification
      • สามารถพบได้ประมาณ 30-50% ในช่วงรายได้ต่ำ ตั้งแต่ต่ำกว่า 2,600 ไปจนถึง 10,400 และค่อยๆ ลดลงไปเรื่อยๆ ตามช่วงรายได้สุทธิ
      • พบจำนวนคนสูบบุหรี่ที่ไม่มีวุฒิการศึกษาในช่วง 2,600-5,200 มากที่สุด คิดเป็น 50% ของกลุ่มตัวอย่างในช่วงนี้
    • A Level
      • คนที่เรียนจบระดับ A-Level พบได้น้อยมากในชุดข้อมูลนี้ โดยคิดเป็นเพียง 1-10% เท่านั้น
      • พบน้อยที่สุดในช่วง 5,200-10,400 ที่ 1.88% ของกลุ่มตัวอย่างในช่วงรายได้
    • GCSE/CSE
      • สวนทางกับผู้ที่เรียนจบระดับอุดมศึกษา กลุ่มนี้สามารถพบได้มากในช่วงรายได้น้อยและลดลงในช่วงรายได้มากขึ้น
      • พบได้มากที่สุดในผู้สูบบุหรี่ที่มีรายได้ต่ำกว่า 2,600 คิดเป็น 27.78% ของกลุ่มตัวอย่าง
    • ONC/BTEC
      • ไม่พบในรายได้ต่ำกว่า 5,200
      • พบมากที่สุดในช่วงรายได้มากกว่า 36,400 คิดเป็น 26.67%
    • ข้อสังเกต:
      • ในช่วงรายได้ปานกลางตั้งแต่ 5,200-28,600 เราสามารถพบคนสูบบุหรี่ได้จากทุกระดับการศึกษา
      • การศึกษาที่สูงมีโอกาสมีรายได้สูงตามด้วยเช่นกัน
      • แต่เราไม่สามารถระบุได้ว่าการมีรายได้สูงหรือการศึกษาสูงมีผลต่อจำนวนคนที่สูบบุหรี่หรือไม่
    Age Range vs Number of Cigarette in Weekday
    Age Range VS Number of Cigarette in Weekday
    Age Range VS Number of Cigarettes in Weekend
    Age Range VS Number of Cigarettes in Weekend

    7. ในส่วนของ bar chart สองอันนี้จะเป็นจำนวนบุหรี่ที่สูบในช่วงวันเสาร์-อาทิตย์ หรือช่วงวันธรรมดา ในแต่ละช่วงอายุ โดยจะสังเกตว่าในวันธรรมดา ผู้หญิงมีแนวโน้มสูบบุหรี่มากกว่าผู้ชายในช่วงอายุ 20-49 ในขณะที่ช่วงวันเสาร์-อาทิตย์ ผู้ชายสูบบุหรี่ในปริมาณที่มากกว่าผู้หญิงในทุกช่วงอายุ ยกเว้นเพียงช่วง 60-69 และ 80-89 เท่านั้น

    • ผู้ชาย
      • มีแนวโน้มสูบบุหรี่มากกว่าผู้หญิงในช่วงวันเสาร์-อาทิตย์ทุกช่วง ยกเว้นช่วงอายุ 60-69 และช่วงอายุ 80-89
      • ไม่พบทั้งผู้ชายและผู้หญิงในช่วงอายุ 90+ สูบบุหรี่เลยในช่วงวันเสาร์-อาทิตย์
      • พบผู้ชายสูบบุหรี่มากกว่าผู้หญิงในช่วง 70-79 โดยคิดเป็นความต่างอยู่ที่ประมาณ 13 คน
    • ผู้หญิง
      • มีแนวโน้มสูบบุหรี่มากกว่าผู้ชายในวันทำงาน ยกเว้นในช่วงอายุ 50-59 และ 60-69 ที่ผู้ชายมีแนวโน้มจะสูบบุหรี่มากกว่า และในช่วงอายุต่ำกว่า 20 ที่มีจำนวนผู้สูบบุหรี่เท่ากัน
      • ไม่พบผู้ชายสูบบุหรี่ในช่วงอายุ 90+
      • ผู้หญิงมีจำนวนสูบบุหรี่มากกว่าผู้ชายมากกว่า 20 คนในช่วงอายุ 40-49
    • ข้อสังเกต:
      • จากกราฟไม่สามารถสรุปได้ว่าเพศและวันหยุดมีความสัมพันธ์กันหรือไม่
      • ยิ่งอายุมากขึ้น มีแนวโน้มที่จะสูบบุหรี่น้อยลง
      • ไม่สามารถระบุความสัมพันธ์ได้
    Marital_status Vs  Average number of cigarettes
    Marital_status Vs Average number of cigarettes

    8. ในส่วนของกราฟนี้จะเป็นการดูว่าสถานะการสมรสกับจำนวนบุหรี่นั้นมีความสัมพันธ์อย่างไร โดยข้อมูลจำนวนบุหรี่นั้นเกิดจากการสร้าง Calculated Field ของค่าเฉลี่ยจำนวนบุหรี่ที่สูบในช่วงวันธรรมดาและวันหยุดเสาร์-อาทิตย์มารวมกัน

    • ผู้ที่มีสถานะโสดมีค่าเฉลี่ยของบุหรี่ที่สูบต่ำที่สุด ประมาณ 28 มวน และสูงสุดคือผู้ที่เป็นหม้าย อยู่ที่ 33 มวน
    • ในแต่ละสถานะการสมรสไม่ได้มีผลต่อจำนวนบุหรี่ที่สูบมากนัก เพราะว่าในกลุ่มตัวอย่างอื่นๆ ก็มีจำนวนที่ใกล้เคียงกัน
    • ข้อสังเกต
      • ไม่ได้พิจารณาปัจจัยอื่นๆ เช่น อายุ ระยะเวลา และเศรษฐกิจ
      • การใช้ค่าเฉลี่ยอาจเป็นค่ากลางที่ไม่ดีที่สุด หากการกระจายตัวนั้นไม่เป็นแบบ Normal Distribution

    Summary

    • อายุและเพศ
      • ผู้หญิงมีแนวโน้มสูบบุหรี่มากกว่าผู้ชายในช่วงวันธรรมดา
      • การสูบบุหรี่ลดลงตามอายุ
    • ปัจจัยทางเศรษฐกิจและการศึกษา
      • บุหรี่แบบซองเป็นที่นิยมในทุกช่วงรายได้
      • รายได้และการศึกษาแปรผันตามกัน
      • ผู้ที่มีรายได้น้อยเลือกที่จะมวนเอง
    • สถานภาพการสมรส
      • คนโสดมีแนวโน้มสูบบุหรี่น้อยที่สุด
      • สถานภาพกับจำนวนบุหรี่ไม่ได้มีนัยสำคัญที่ชัดเจน

    Restriction

    • ไม่มีข้อมูลช่วงเวลา จึงไม่สามารถทำ Time Series Chart ได้ ส่งผลให้ไม่รู้ว่าในระยะยาวนั้นสรุปข้อมูลได้อย่างไร
    • ข้อมูลยังน้อยไป ไม่สามารถหาความสัมพันธ์ที่ชัดเจนได้
    • ไม่มีข้อมูลบางด้าน เช่น อาชีพที่อาจจะสามารถสะท้อนระดับความเครียดกับจำนวนบุหรี่ได้

    How can I make it better

    • เก็บข้อมูลเพิ่มเติมโดยอ้างอิงตามช่วงเวลา เพื่อดูแนวโน้มการเปลี่ยนแปลงในระยะยาว
    • เก็บข้อมูลเพิ่มเติม เช่น อาชีพ และระดับความเครียด

    References

    https://www.kaggle.com/datasets/utkarshx27/smoking-dataset-from-uk/data

    https://www.bbc.com/news/health-68825322

  • R 101 สรุป สิ่งที่ได้เรียนรู้เพิ่มเติมจาก Hands-On Programming with R

    R 101 สรุป สิ่งที่ได้เรียนรู้เพิ่มเติมจาก Hands-On Programming with R

    หากใครก็ตามที่คิดว่าจะเริ่มวิเคราะห์ข้อมูล ทำกราฟ หรืออะไรก็ตามแต่ ในยุคที่เทคโนโลยีนั้นเปลี่ยนผ่านไปอย่างรวดเร็ว ใครๆ ก็ล้วนนึกถึง Python หนึ่งในภาษาที่มีความยืดหยุ่นอย่างมาก สามารถวิเคราะห์ข้อมูล ทำนายผล และยังสามารถแสดงกราฟออกมาได้ แต่ในสมัยที่ยังไม่มี Python เราใช้อะไรในการทำกันล่ะ?

    ภาษา R ถูกสร้างขึ้นในปี 1933 โดยมีจุดประสงค์เพื่อใช้งานในการคำนวณเชิงสถิติและการทำกราฟแบบต่างๆ โดยในปัจจุบันได้มีการพัฒนาส่วนเสริม (packages) ขึ้นมาเป็นจำนวนมากเพื่อให้รองรับการใช้งานสำหรับชาว Data Scientist

    Photo by Pixabay on Pexels.com

    บทความนี้เป็นการนำสิ่งที่ผมได้เรียนรู้จากการอ่านหนังสือ ที่ชื่อว่า Hands-On Programming Project 1 และ 2 มาแชร์ให้เพื่อนๆฟังกันครับ การบ้านจากคอร์ส Data Science Bootcamp ของ DataRockie

    Table of Contents

    1. Weighted Dice

    ภายในหนังสือนั้น จะนำเสนอตัวอย่างโค้ดและ syntax ผ่านการทดลองทำโปรเจกต์ง่ายๆ อย่างการทำลูกเต๋าสองลูกที่ถูกถ่วงน้ำหนัก ภายในหัวข้อนี้จะครอบคลุมเนื้อหาในส่วนที่เป็นพื้นฐาน ซึ่งหากคุณค่อยๆ ทำตามจะสามารถเข้าใจหลักการเขียนคำสั่งต่างๆ ดังนี้

    • การเขียนคำสั่ง R
    • การสร้าง R Object
    • การเขียนฟังก์ชัน
    • การโหลดใช้งานส่วนเสริม packages
    • การสุ่ม
    • การพล็อตกราฟอย่างง่าย

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

    Garrett Grolemund

    ในส่วนแรกของหนังสือ เราต้องโหลด RStudio Desktop และ R language ก่อนเพื่อเริ่มทำโปรเจคนี้

    Rstudio Interface

    ภาษา R เป็น ภาษาประเภท dynamic-programming ภาษาที่คุณไม่จำเป็นจำต้อง compile เพื่อเปลี่ยนมันเป็นสิ่งที่คอมพิวเตอร์เข้าใจ เช่น ภาษา C หรือ JAVA

    Basic Calculation Command

    R สามารถทำ + - * / ได้เหมือนกับภาษาทั่วๆไป

    10 + 2
    ## 12
    
    12 * 3
    
    36 - 3
    ## 33
    
    33 / 3
    ## 11
    
    

    Object

    หากเราต้องการสร้าง ลูกเต๋า เราสามารถใช้ : เพื่อสร้าง vector data type ที่เก็บค่าแค่มิติเดียว

    1:6
    
    ## 1 2 3 4 5 6
    
    

    แต่ว่าการที่เราไม่ได้ assign ค่าให้เข้ากับ ตัวแปรบางอย่างจะทำให้เราไม่สามารถใช้งานมันได้ เนื่องจากไม่ได้บันทึกค่า ลงใน computer’s memory โดยเราสามารถบันทึกค่า ลงตัวแปร object ได้ดังนี้

    a <- 1
    
    die <- 1:6 # ทำการบันทึก vector 1:6 ลงในตัวแปร die
    
    

    เครื่องหมาย < - จะบอกให้คอมพิวเตอร์ทำการ assign ค่า ลงไปในตัวแปรทางซ้ายมือของเรา

    R เป็น ภาษา case-sensitive เพราะฉะนั้น Name กับ name ถือว่าเป็นคนละตัวแปร

    R ห้ามตั้งชื่อโดยขึ้นต้นด้วยตัวเลข และ ห้ามใช้ special case ด้วย

    เราสามารถใช้คำสั่ง ls() เพื่อดูว่าเราได้ประกาศตัวแปรอะไรไปแล้วบ้าง

    ในภาษา R นั้น จะไม่ได้ยึดหลักการ คูณ Matrix เหมือนใน Linear Algebra แต่จะใช้วิธีการที่เรียกว่า element-wise execution ซึ่งเป็นการทำ operation โดยจับคู่ vector สองอันมาเทียบกัน

    หาก vector สองอันที่เอามาใช้ ยาว ไม่เท่ากัน R จะทำการวนลูปจน ตัวที่สั้นไปบนตัวที่ยาวจนครบ แล้วค่อยทำ operation เราเรียกว่า vector recycling

    https://rstudio-education.github.io/hopr/basics.html

    เราสามารถใช้ %*% สำหรับการคูณแบบเมทริกซ์ และ %o% สำหรับการคูณแบบ outer product

    Function

    ภายใน R มี built-in ฟังก์ชัน ให้ใช้งานอยู่หลายแบบ ไม่ว่าจะเป็น round, factorial, mean

    R จะรัน ฟังก์ชัน จากในสุดออกไปด้านนอก ดังตัวอย่าง

    สำหรับใน การ สุ่มเต๋านั้น เราจะใช้ sample ฟังก์ชัน ที่รับ argument 2 ค่า โดย รับเป็น vector กับ จำนวนผลลัพธ์ที่จะ return ออกมา

    sample(x = die, size = 1)
    ## 2
    
    

    ในหลายๆครั้ง เรา ไม่รู้หรอกว่า ฟังก์ชัน ดังกล่าวรับ argument ชื่ออะไรบ้าง เพราะฉะนั้นเราสามารถใช้คำสั่ง args() เพื่อเช็คได้

    หากเราสังเกตจะพบว่า หากเราไม่เรียกใช้ parameter replace ลูกเต๋าของเราก็จะไม่สุ่มหน้าซ้ำ ซึ่งมันก็จะผิดหลักการการสุ่มลูกเต๋า

    อย่างไรก็ตาม การที่เราต้องมานั่ง run commands อย่าง sample และ sum ทุกครั้งมันก็คงจะเป็นการทำที่น่าเบื่อหน่าย เพราะฉะนั้นเราจึงจะสร้าง ฟังก์ชัน ของเรามาใช้เอง

    my_function ← function() {}

    roll <-function() {
      die <- 1:6
      dice <- sample(die, size = 2, replace = TRUE)
      sum(dice)
    }
    
    

    โค้ดที่เรานำไปวางไว้ภายในวงเล็บปีกกาเรียกว่า body ของ ฟังก์ชัน นั้นๆ เพราะฉะนั้น บรรทัดสุดท้ายของโค้ด จะต้องคืนค่าบางอย่างกลับมา อย่างไรก็ตาม จากตัวอย่างข้างต้น การที่เราจะต้องมาประกาศตัวแปร die ทุกครั้งที่เรียกฟังก์ชั่น ก็กระไรอยู่ ทำไมเราไม่นำมาไว้ข้างนอก แล้วเอาส่งเข้าไปล่ะ

    bones <- 1:6
    roll <-function(bones) {
      dice <- sample(x= bones, size = 2, replace = TRUE)
      
    }
    
    roll(bones)
    
    

    อย่างไรก็ตาม ในกรณีนี้หากเรา ไม่ได้ระบุค่า bones จะทำให้ ฟังก์ชัน ของเรา error เพราะฉะนั้นเราควรกำหนดค่า default ให้ funciton

    roll2 <-function(bones = 1:6) {
      dice <- sample(bones, size = 2, replace = TRUE)
      sum(dice)
    }
    
    

    ใน R คุณสามารถสร้าง ฟังก์ชัน ด้วยตัวเองมากแค่ไหนก็ได้ แตกต่างจาก tools สำเร็จรูปอย่าง excel คุณเคยสร้างเมนูใหม่ใน Excel หรือไม่ เพราะฉะนั้นการที่คุณสามารถเขียน programming ได้เอง จะช่วยให้คุณเปิดกว้างได้มากขึ้น

    R สามารถเก็บค่าประเภท binary และ ค่า complex number ได้เช่นกัน

    2. Playing Card

    ภายในโปรเจคที่สอง นี้เราจะมาสร้างการ์ดเกมโดยคุณจะได้เรียนรู้การ เก็บ เรียกใช้ และการเปลี่ยนแปลค่าของข้อมูลใน computer’s memory

    สกิลเหล่านี้จะช่วยให้คุณ จัดการกับข้อมูลโดยปราศจาก error เราจะออกแบบเด็ดสำหรับการเล่นที่เราสามารถแจกหรือสลับก็ได้ โดยเด็ดนี้จะจำว่าการ์ดใบไหนได้แจกไปแล้วบ้าง

    • ในโปรเจคนี้เราจะได้เรียนการบันทึกประเภทข้อมูล เช่น string และ boolean
    • บันทึกข้อมลในรูปแบบ vector matrix array list และ dataframe
    • โหลดและบันทึกข้อมูลด้วย R
    • รับข้อมูลจาก data set
    • เปลี่ยนค่าข้อมูลใน data set
    • เขียน เทสสำหรับ logics
    • การใช้ NA

    Task 1: build the deck

    atomic vector – simple vector ที่เราได้เรียนไปในบทที่ 1 อย่าง die โดยเราสามารถใช้คำสั่ง is.vector เพื่อเช็คประเภทข้อมูลได้ โดยการบันทึกค่าเพียงค่าเดียวก็นับเป็น atomic vector เช่นกัน

    เราสามารถเขียน 1L ภายใน atomic vector เพื่อระบุค่าว่าเป็น number vecotr ได้

    length เป็น ฟังก์ชั่นที่ใช้ในการนับความยาวของ atomic vector

    เราสามารถใช้ typeof เพื่อระบุประเภทของข้อมูลว่าเป็นข้อมูลประเภทอะไรใน R

    คำว่า doubles และ numerics นั้นมีความหมายเหมือนกัน แต่ว่าคำว่า double เป็นคำที่นิยมใช้ในแวดวง programming เพื่อสื่อถึงจำนวน bytes ที่คอมพิวเตอร์ใช้ในการเก็บค่าข้อมูล

    โดยปกติแล้วหากเราไม่ระบุ L. ท้ายตัวเลขนั้น R จะบันทึกค่าเป็น double โดยค่ายังเหมือนกัน แตกต่างที่ type เท่านั้น อย่างไรก็ตามเราไม่สามารถบันทึกทุกค่าเป็น doubles ได้เพราะว่าคอมพิวเตอร์จะใช้ 64bits สำหรับการเก็บ ค่าdoubles เพราะฉะนั้น ค่า doublesใน R จะมีทศนิยมแค่ 16.ตำแหน่งเท่านั้น

    ซึ่งเหตุการณ์เหล่านี้ เรียกว่า floating-point error ซึ่งคุณสามารถพบได้เมื่อทำการคำนวณค่าด้วย ตัวเลขที่มีทศนิยม เราเรียกปัญหานี้ว่า floating-point arithmetics

    เราสามารถเขียน TRUE FALSE ใน R ด้วย T F ได้เช่นกัน

    เราสามารถใช้คำสั่ง attributes เพื่อเช็คว่า object ดังกล่าวมี attributes อะไรบ้าง โดย NULL คือค่าที่จะแสดงออกมในกรณีที่เป็น null set หรือ empty object

    ฟังก์ชัน names() เป็น ฟังก์ชั่นที่ใช้ในการตั้งชื่อ ให้กับ atomic vector, dim, classes

    อย่างเช่นเราสามารถตั้งชื่อให้ค่าในลูกเต๋าเราได้

    names(die) <- c("one", "two", "three", "four", "five", "six")
    ##  one   two three  four  five   six 
    ##    1     2     3     4     5     6 
    
    

    แต่ว่าหากเรา +1 ค่าใน die ชื่อก็จะไม่ได้เปลี่ยนตามอยู่ดี โดยหากต้องการนำออกให้ assign เป็น NULL

    เราสามารถเปลี่ยน ค่า atomic vector ไปเป็น dimensional array โดย assign ค่า number vector ไปที่ attribute dim

    โดย ค่าที่อยู๋ dimensional array นั้นจะเริ่มจากค่า rows ก่อนเสมอ

    เราสามารถสร้าง matrix ด้วยการเรียกใช้ ฟังก์ชัน ที่ชื่อว่า matrix โดยการป้อน argument number vector, จำนวนแถว และค่า boolean สำหรับการบอกว่าให้เติมแบบแถวต่อแถวหรือไม่

    m <- matrix(die, nrow = 2, byrow = TRUE)
    
    ##    [,1] [,2] [,3]
    ## [1,]    1    2    3
    ## [2,]    4    5    6
    
    
    

    การที่เราเปลี่ยนแปลง dimension ของ object นั้นจะไม่ได้เปลีย่น type แต่จะเปลี่ยน class ของ object นั้นแทน

    dim(die) <- c(2, 3)
    typeof(die)
    ##  "double"
     
    class(die)
    ##  "matrix"
    
    

    เราสามารถเรียกใช้ Sys.time() เพื่อรับค่าเวลาได้ แต่ว่า type ของมันจะเป็น double และ class จะเป็น POSIXct POSIXt

    POSIXct เป็น framework ที่คนนิยมใช้เพื่อการแสดงค่าวันที่และเวลา โดย เวลาใน POSIXct จะแสดงค่าด้วยจำนวนเวลาที่ได้ผ่านไปแล้วนับตั้งแต่วันที่ January 1st 1970

    หากเราใช้คำสั่ง unclass เราก็จะพบกับค่าตัวเลขที่ อยู่เบื้องหลัง วันที่และเวลา ในทางกลับกัน เราก็สามารถ assign class ให้กับตัวเลขเพื่อให้ได้วันที่ได้เช่นกัน

    now <- Sys.time()
    unclass(now)
    ## 1395057600
    
    mil <- 1000000
    mil
    ## 1e+06
    class(mil) <- c("POSIXct", "POSIXt")
    mil
    
    
    

    factors คือ ฟังก์ชัน ที่เราไว้ใช้เก็ฐค่าประเภท categorical information.

    Coercion

    เป็นหลักการที่ R จะทำการเปลียนค่าของ data types เช่น หากมีค่า character string ใน atomic vector R จะเปลี่ยนค่าทั้งหมดเป็น string

    หากเป็นค่า logical และมี numerics อยู่ก็จะเปลี่ยนเป็น 0 หรือ 1 แทน

    เราสามารถเรียกช้คำสั่ง as.character(gender) เพื่อให้ค่าปลี่ยนเป็น character string

    https://rstudio-education.github.io/hopr/images/hopr_0301.png

    อย่างไรก็ตาม การเรียนรู้ data types ด้านบนนั้นไม่สามารถนำมาใช้ในการสร้าง card deck ได้เพราะพวกมันเก็บ data type ได้แค่แบบเดียว

    list สามารถเก็บค่า ได้หลายรูปแบบ เช่น

    card <- list("ace", "hearts", 1)
    card
    ## [[1]]
    ## [1] "ace"
    ##
    ## [[2]]
    ## [1] "hearts"
    ##
    ## [[3]]
    ## [1] 1
    
    

    list สามารถเก็บค่า การ์ดทั้งสำรับให้เราได้แต่เราก็คงจะไม่ทำเช่นกัน นั้นจึงเป็นที่มาของ Data Frames two-dimensional version of a list.

    ค่า character string ด้านในที่บันทึกไว้จะเป็น factors ทั้งหมด หากคุณไม่อยากได้สามารถกำหนด parameter ที่ชื่อว่า stringsAsFactors ให้เป็น false

    df <-data.frame(face =c("ace", "two", "six"),
      suit =c("clubs", "clubs", "clubs"), value =c(1, 2, 3))
    df
    ## face  suit value
    ##  ace clubs     1
    ##  two clubs     2
    ##  six clubs     3
    
    

    ในการสร้าง data frames นั้นคุณจำเป็นต้องมั่นใจว่า ข้อมูลของคุณนั้นยาวเท่ากันในแต่ละ vector ไม่เช่นนั้นคุณต้องทำ recycling rules เพื่อให้มีคววามยาวเท่ากัน

    สำหรับ data frame แล้ว มี type เป็น list แต่ว่าหากเราเช็ค class เราก็จะได้เป็น data.frame

    ในแบบทดสอบนี้ในหนังสือได้เตรียมไฟล์ csv ไว้ให้แล้วเนื่องจากการที่เราต้องามาสร้าง deck เองเนี้ยโอกาสเกิด error ค่อนข้างเยอะจึงไม่ควรพิมพ์เยอะขนาดนั้น

    โดยเราสามารถเรียกใช้คำสั่ง read.csv ในการอ่านไฟล์ได้

    Task 2: 2 functions

    ในการเล่นเกมการ์ดนั้น เราต้องสามารถสลับกองไพ่และจั่วใบบนสุดเพื่อแจกได้ เพราะฉะนั้นเราต้องเขียน ฟังก์ชัน สองอันเพื่อมาารองรับ การทำงานสองสิ่งนี้

    • Positive integers deck[1, c(1, 2, 3)]
      • ถ้าเราเขียนซ้ำด้วยการใส่ vector ลงไป R ก็จะแสดงค่าซ้ำด้วย
    • Negative integers deck[-2(2:52), 1:3]
      • จะดึงค่าที่เราไมได้กำหนดอออกมา เช่นกรณีก็จะดึงมาแค่ king spades 13
      • ง่ายต่อการทำ subset
      • เราสามารถใส่ จำนวนเต็มบวก และ ลบ ใน vector เดียวกันเพื่อดึงค่าออกมา deck[c(-1, 1), 1]
    • Zero – แม้ว่าจะไม่ใช่ทั้งจำนวนเต็มบวกและลบ แต่่คุณก็จะได้ data frame เปล่ามาอยู่ดี deck[0, 0] ## data frame with 0 columns and 0 rows
    • Blank spaces – การเว้นว่างไว้ เป็นการบอก R ให้ว่า เอาค่าในทุกๆอันใน row หรือ column มา
    • Logical values การใส่ atomic vector ของ logic ลงไป R จะพยายาม match ค่า แล้วดึงข้อมูลออกมาแค่เฉพาะค่าที่ ป็น True เท่านั้น

    deck[ , "value"]

    deck[1, **c**("face", "suit", "value")]

    • Names – เราสามารถดึงข้อมูลด้วย ชื่อ ได้เช่นกัน

    โดยปกติแล้วเราก็จะใช้ deck[ , ] ในการนำค่าออกมา โดย หลักแรกจะแทน row และหลักสองจะแทน column เราสามารถสร้าง index ได้หลายรูปแบบดังนี้

    ใน R นั้น index จะเริ่มที่ 1 ไม่ใช่ 0

    ในกรณีท่ี่เรา เลือกค่ามาแค่ 1 column เราจะได้แค่ เป็น vector หากอยากได้ data frames เราต้องกำหนด drop = False

    ทีนี้เราจะมาสร้าง ฟังก์ชัน สลับการ์ดกัน

    deal <- ฟังก์ชัน(cards) {
    	cards[1, ]
    }
    
    

    เบื้องต้นหากเราใส่ 1 ลงไป เราก็จะได้การ์ดใบบนสุดของ deck มาแต่ ว่าในการเล่นจริงนั้น เราต้องการให้กองไพ่นั้น สลับและไม่อยู่ใน ลำดับเดิมตลอดเวลา เพราะฉะนั้นแทนที่เราจะใส่ 1:52 เราก็จะใส่เป็น random order เข้าไปแทน

    shuffle <- ฟังก์ชัน(cards) {
      random <- sample(x= 1:52, size = 52)
      cards[random,]
    }
    
    

    ทีนี้เราก็สามารถสลับการ์ดหลังจาก เราแจกไพ่ได้แล้ว

    deal(deck)
    ## face   suit value
    ## king spades    13
    
    deck2 <-shuffle(deck)
    
    deal(deck2)
    ## face  suit value
    ## jack clubs    11
    
    

    เราสามารถดึงค่ามาเป็น vector ได้ด้วย $ เช่น deck$value สิ่งนี้จะมีประโยชน์มากเพราะว่าเราสามารถใช้ค่า numeric vector ในการ ทำงานด้าน math ได้ เช่น mean หรือ median

    เราสามารถใช้ [[]] ในการ subset ค่าได้เช่นกันในกรณีที่เราไม่มีชื่อ ให้กับ list นั้น

    lst[[1]]
    ## 1 2
    
    

    หากเรา ใช้แค่ [] เราจะได้ list ของ column แต่หากใช้ [[ ]] เราจะได้ค่าของ vector นั้นแทน

    Task 3: change the point system to suit your game

    เราสามารถ assign ค่าเข้าไปใน atomic. vector ได้

    vec[1] <- 1000
    vec
    ## 1000    0    0    0    0    0
    
    

    ด้วยวิธีนี้เราก็สามารถสร้าง column ใหม่ได้

    deck2$new <- 1:52
    
    deck2$new <- NULL
    
    

    โดยปกติแล้วการ ดึงข้อมูลและแก้ข้อมูลเป็นเรื่องง่ายหากเรารู้ตำแหน่ง แต่หากเราไม่รู้ เราก็จำเป็นที่จะต้องนำ การทำ subset ด้วย logic เข้ามาช่วย

    deck3$value[deck3$face == "ace"] <- 14
    
    
    OperatorSyntaxTests
    &cond1 & cond2Are both cond1 and cond2 true?
    ```cond1
    xorxor(cond1, cond2)Is exactly one of cond1 and cond2 true?
    !!cond1Is cond1 false? (e.g., ! flips the results of a logical test)
    anyany(cond1, cond2, cond3, ...)Are any of the conditions true?
    allall(cond1, cond2, cond3, ...)Are all of the conditions true?
    https://rstudio-education.github.io/hopr/modify.html
    OperatorSyntaxTests
    >a > bIs a greater than b?
    >=a >= bIs a greater than or equal to b?
    <a < bIs a less than b?
    <=a <= bIs a less than or equal to b?
    ==a == bIs a equal to b?
    !=a != bIs a not equal to b?
    %in%a %in% c(a, b, c)Is a in the group c(a, b, c)?
    https://rstudio-education.github.io/hopr/modify.html

    บ่อยครั้งที่ค่าบางค่านั้นหายไป ใน R เราจะใช้ NA ในการบอกว่า not available NA ช่วยให้ ข้อมูลของเราสามารถนำมาทำด้านสถิติได้แต่บางครั้งเวลาหาค่าทางคณิตศาสตร์ อาจจะทำให้ค่า error ได้ เพราะฉะนั้น บาง ฟังก์ชัน จะมี parameter อย่าง na.rm ที่ให้เรา set True เพื่อไม่นำมาคำนวณ

    เราสามารถใช้ is.na() ในการหาว่าใน data type นั้นมีค่า NA หรือไม่

    Task 4: manage the state of the deckฃ

    R เก็บข้อมูลใน environment คล้ายกับเวลาเราเก็บไฟล์ใน โฟลเดอร์ต่าง เราสามารถใช้ parevns ฟังก์ชั่น ใน package pryr

    ใน environment tree เราสามารถเข้าถึงค่า globalenv() baseenv() emptyenv() ได้

    เราสามารถใช้ ls หรือ ls.str ในการดู objects ที่บันทึกใน environments โดย ls จะให้ดูแค่ชื่อ แต่ ls.str จะดู structure.ได้ด้วย

    โดยค่าทั้งหมดที่ เราได้ บันทึกนั้นจะอยู่ใน global environment และเรายังสามารถใช้ assign() เพื่อ เพิ่มค่า ลงไปใน global environment ได้เช่นกัน

    โดยทั่วไปแล้ว global environment จะเป็น active environment แต่หากเรา รัน ฟังก์ชัน บางอย่างอาจทำให้ค่า active environment เปลี่ยนไปได้

    ใน R. เวลา R หาค่านั้นจะมีกฎอยู่ 3 ข้อ

    1. R จะหาค่าใน active environment ก่อน
    2. ถ้าทำงานใน CLI global environment จะเป็น active environment
    3. หาก R หาไม่เจอใน environemnt R จะมองหา object นั้น parent environment
    https://rstudio-education.github.io/hopr/images/hopr_0603.png

    เวลาที่เรา run ฟังก์ชัน R จะสร้าง environment ขึ้นมาใหม่ที่ runtime เกิดขึ้น หลังจากนั้นจะเอา result ส่งกลับไป environment ที่เราบันทึก ฟังก์ชัน ไว้

    อย่างเช่นใน กรณีตัวอย่างนี้ เราจะพบว่า ตอนรันข้อมูลด้านใน ฟังก์ชั่น show_env active environment จะเป็นอันที่อยู่ภายใต้ GlobalEnv หรือก็คือ parent environment

    show_env <- ฟังก์ชัน(){
      list(ran.in = environment(), 
           parent = parent.env(environment()), 
           objects = ls.str(environment()))
    }
    
    show_env()
    
    

    global environment ไม่จำเป็นต้องเป็น parent ยกตัวอย่างเช่น หากเราเรียก ฟังก์ชัน parenvs() parent env ก็จะมาจาก pryr ซึ่งก็คือที่ๆ ตัวฟังก์ชั่นนี้ถูกสร้างครั้งแรกนั้นเอง

    และด้วยเหตุนี้ มันก็จะมี ฟังก์ชัน block เพราะว่า R ก็จะบันทึก object ไว้ใน runtime environment เพื่อให้มั่นใจว่าไม่มีค่าไหนใน global ถูกเขียนทับ

    ในกรณีที่มีการส่ง arguments R ก็จะ copy ค่าเข้าไปใน runtime environment ด้วยเช่นกัน

    สรุปก็คือ สมมติว่าเรากำลังรันโค้ดใน ภาษา R อยู่ตอนแรก โค้ดเราก็จะรันใน active environment ซึ่งเป็น environment ที่ใช้ call ฟังก์ชัน เมื่อเรียก ฟังก์ชัน R จะทำการ ตั้ง runtime environment แล้ว copy ค่าใน argument เข้ามาใน environment นี้ด้วย หลังจากนั้นก็จะรันโค้ดใน ฟังก์ชัน body หากมีการ assign ค่าก็ เก็บไว้ใน runtime environment หากมีการเรียกใช้ object ที่ไม่มีใน runtime environment ก็จะทำตาม scoping rule แล้วไล่หาใน parent environment หลังจากที่รัน ฟังก์ชัน เสร็จแล้ว R ก็จะเปลี่ยนกลับมาใช้ environment ที่เรียก ฟังก์ชัน หากมีการ assign ค่าที่ return จากฟังก์ชั่นเราก็จะ เอาค่านั้นมาบันทึกที่ environment

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

    หากเราต้องการจะทำให้มั่นใจว่า สำรับที่เราเล่นจะไม่ถูกทำให้หายระหว่างที่เล่น เราก็สามารถสร้าง ฟังก์ชัน setup ขึ้นมาเพื่อสร้างอีก environment นึงในเก็บค่าได้เช่นกัน

    setup <-ฟังก์ชัน(deck) {
      DECK <- deck
    
      DEAL <-ฟังก์ชัน() {
        card <- deck[1, ]
    assign("deck", deck[-1, ], envir =parent.env(environment()))
        card
      }
    
      SHUFFLE <-ฟังก์ชัน(){
        random <-sample(1:52, size = 52)
    assign("deck", DECK[random, ], envir =parent.env(environment()))
     }
    
    list(deal = DEAL, shuffle = SHUFFLE)
    }
    
    cards <-setup(deck)
    deal <- cards$deal
    shuffle <- cards$shuffle
    
    

    ทีนี้หากเราเผลอลบ deck ในชั้น globalไปเราก็ยังเล่นได้อยู่ดี

    และนั่นคือทั้งหมดของ Hands-ON Programming with R ใน chapter 1 และ 2 ในบทความถัดไปเราจะมาพูดถึง สิ่งที่ผมได้เรียนรู้เพิ่มเติมกันครับ

    References

    https://rstudio-education.github.io/hopr/environments.html