สวัสดีคุณผู้อ่านทุกท่านค้าบ 😆 ฮี่ๆ วันนี้พามาใช้ R ในการทำโมเดล Logistic regression กันฮะ โดยใช้ข้อมูลที่ Data Analyst น่าจะเคยผ่านหูผ่านตากันมาบ้างสักครั้งนึง เพราะ “ถ้าคุณโดด ผมโดด” 😭 และสุดท้ายหนุ่มต้องการจะสร้าง App เพื่อ Role play เป็นคนในเรือดูว่าจะรอด ไม่จม ไม่แข็งตายไปรึป่าว

โดดลงน้ำไปเลยจ้า :: Scene Description Spotlight: “Titanic”

กดไล่ดูตามหัวข้อได้เลยคับ

🤝 รู้จักข้อมูลกันก่อน
🥘 Prep ข้อมูลให้พร้อมใช้ในโมเดล
🚂 Train Model: พี่จะสอนน้องเอง
🧪 Test Model: ลองดูก่อน
🍌มา Role play แบบเรียลไทม์กัน

🤝 รู้จักข้อมูลกันก่อน

Data set นี้มาจาก library(titanic) ของ R มีเรคคอร์ดทั้งหมด 891 rows เป็น 891 คนที่ได้ขึ้นเรือไทนานิคและเก็บข้อมูลมาได้ (มั้งง?)

ColumnsDescription
PassengerIdหมายเลขประจำตัวผู้โดยสาร (Unique ID)
Survivedสถานะการรอดชีวิต (0 = ไม่รอด, 1 = รอด)
Pclassระดับชั้นโดยสาร (1 = ชั้น 1, 2 = ชั้น 2, 3 = ชั้น 3)
Nameชื่อผู้โดยสาร
Sexเพศ (male, female)
Ageอายุ
SibSpจำนวนพี่น้อง/คู่สมรสที่เดินทางมาด้วย
Parchจำนวนพ่อแม่/ลูกที่เดินทางมาด้วย
Ticketหมายเลขตั๋วโดยสาร
Fareค่าโดยสาร
Cabinหมายเลขห้องโดยสาร
Embarkedท่าเรือที่ขึ้นเรือ (C = Cherbourg, Q = Queenstown, S = Southampton)

มาจับโค้ดดูข้อมูลกันซักนิดว่าเบื้องต้น เช่น ขนาดข้อมูลมีเท่าไหร่, ตัวแปรที่มีเป็นประเภทไหนเป็น Qualitative เชิงคุณภาพ หรือ Quantitative เชิงปริมาณ, มีช่องว่างหรือค่าที่หายไป (missing values) ในคอลัมน์มั้ย

# Import Library and Dataset Titanic
library(tidyverse)
library(titanic)

summary(titanic_train)
count(titanic_train)

ในตัวชุดข้อมูลมี หมายเลขประจำตัวผู้โดยสาร (PassengerId), สถานะการรอดชีวิต (Survived), ระดับชั้นโดยสาร (Pclass), ชื่อผู้โดยสาร (Name), เพศ (Sex), หมายเลขตั๋วโดยสาร (Ticket), หมายเลขห้องโดยสาร (Cabin) และ ท่าเรือที่ขึ้นเรือ (Embarked) ที่เป็นตัวแปรเชิงคุณภาพ (Qualitative variables) นอกนั้นเป็นตัวแปรเชิงปริมาณ (Quantitative Variables)

ตรวจสอบค่าว่าง กับค่า na กันซักหน่อย เพราะมันส่งผลต่อโมเดลเหมือนกัน

# ตรวจสอบค่า NA โดยใช้ sapply()
sapply(titanic_train, function(x) sum(is.na(x)))

# ตรวจสอบค่าว่าง "" โดยใช้ sapply
sapply(titanic_train, function(x) sum(x == ""))

จะเห็นว่าแค่ตรวจ na อย่างเดียวไม่ได้ต้องตรวจค่าว่าง “” ด้วย มีตัวแปร Age, Cabin และ Embarked ที่มีค่าว่างอยู่ และในมุมมองของหนุ่ม หมายเลขประจำตัวผู้โดยสาร (PassengerId), ชื่อผู้โดยสาร (Name), หมายเลขตั๋วโดยสาร (Ticket), หมายเลขห้องโดยสาร (Cabin) และ ท่าเรือที่ขึ้นเรือ (Embarked) ไม่น่าจะส่งผลต่อการรอดชีวิตของการรอดชีวิตในครั้งนี้ ก็เลยจะ ✂️ ตัดตัวแปรนี้ออกไปนะครับ

ตัวแปรที่ต้องจัดการต่อก็จะเหลือเพียงอายุ (Age) เท่านั้น จึงพิจารณาตัดออกเช่นเดียวกันเพราะ Sample size ของเรา (หลังจากตัด age ว่างออกแล้ว) ก็ค่อนข้างจะใหญ่อยู่ โดยใช้ na.omit() ~เหลืออยู่ 714 rows

# copy original dataset to my object
titanic_df <- titanic_train

# Feature Selection:: Drop Not use Columns
titanic_df$PassengerId <- NULL
titanic_df$Name <- NULL
titanic_df$Ticket <- NULL
titanic_df$Cabin <- NULL
titanic_df$Embarked <- NULL

# Feature Selection:: Drop Age.na
titanic_df <- na.omit(titanic_df)

summary(titanic_df)
count(titanic_df) #714 rows

🥘 Prep ข้อมูลให้พร้อมใช้ในโมเดล

ขอแวะจัดข้อมูลสักแป๊ปนะครับ อยากให้หัวคอลัมน์เป็นพิมพ์เล็กทั้งหมด และก็แปลงตัวแปรเชิงคุณภาพที่เหลืออยู่อย่างเพศที่ยังเป็น male, female อยู่ให้เป็นตัวแปรเชิงปริมาณ (numerical variables) โดยใช้ dummy variables และเพิ่มตัวแปร surv แปลงตัวเลขเป็นข้อความ ใช้ตอน explorer ดูแนวโน้มข้อมูล

# Data Transformation
titanic_df <- titanic_df %>%
  select(pclass = Pclass, sex = Sex, age = Age, sibsp = SibSp, parch = Parch, fare = Fare, survived = Survived) %>%
  mutate( sex = if_else(sex=='male', 0, 1),
				  surv = if_else(survived==0, 'Died', 'Survived'))
  
head(titanic_df)

1. ส่อง Age: ค่า median ของอายุอยู่ที่เกือบถึง 30 ปี (~ประมาณ 28-30 ปี) ไม่ว่าจะ Died หรือ Survived โดยอายุมากสุดอยู่ที่ประมาณ 80 ปี+

# boxplot:: Age
titanic_df %>% 
  ggplot(aes(age)) +
  geom_boxplot()

# boxplot:: Age+surv
titanic_df %>% 
  ggplot(aes(x=age, y=surv)) +
  geom_boxplot()

2. ส่อง Fare: ในส่วนของค่าโดยสารมี median อยู่ 15USD มากสุดจ่ายอยู่ที่ 500USD++ หากดูจากกราฟดูเหมือนว่าคนที่จ่ายค่าโดยสารสูงจะมีโอกาสรอดมากกว่านะเนี่ย 🤔

# boxplot:: Fare
titanic_df %>% 
  ggplot(aes(fare)) +
  geom_boxplot()

# boxplot:: Fare+surv
titanic_df %>% 
  ggplot(aes(x=fare, y=surv)) +
  geom_boxplot()

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

# bar:: pclass
titanic_df %>% 
  ggplot(mapping = aes(x=pclass)) +
  geom_bar(fill = 'salmon', alpha=0.6)

# bar:: pclass+surv
titanic_df %>% 
  ggplot(mapping = aes(x=pclass)) +
  geom_bar(fill = 'salmon', alpha=0.6) +
  facet_wrap(~surv, ncol=2)

4. ส่อง Sex: จากที่เราได้ mutate sex ให้ male เป็น 0 และ female เป็น 1 แล้ว จะเห็นว่า male มีจำนวนผู้เสียชีวิตมากกว่า female ประมาณ 4 เท่า

# bar:: sex
titanic_df %>% 
  ggplot(mapping = aes(x=sex)) +
  geom_bar(fill = 'salmon', alpha=0.6)

# bar:: sex+surv
titanic_df %>% 
  ggplot(mapping = aes(x=sex)) +
  geom_bar(fill = 'salmon', alpha=0.6) +
  facet_wrap(~surv, ncol=2)

5. ส่อง Sibsp และ Parch: จำนวนพี่น้อง/คู่สมรสที่เดินทางมาด้วย และ จำนวนพ่อแม่/ลูกที่เดินทางมาด้วย มีความถี่ที่มากสุดคือ มาคนเดียวนะครับ ที่เป็นแบบนี้อาจเป็นเพราะข้อมูลนี้นับรวม cabin crew ด้วยที่ต้องขึ้นมาทำงานคนเดียว

# bar:: sibsp
titanic_df %>% 
  ggplot(mapping = aes(x=sibsp)) +
  geom_bar(fill = 'salmon', alpha=0.6)

# bar:: sibsp+surv
titanic_df %>% 
  ggplot(mapping = aes(x=sibsp)) +
  geom_bar(fill = 'salmon', alpha=0.6) +
  facet_wrap(~surv, ncol=2)

# bar:: parch
titanic_df %>% 
  ggplot(mapping = aes(x=parch)) +
  geom_bar(fill = 'salmon', alpha=0.6)

# bar:: parch+surv
titanic_df %>% 
  ggplot(mapping = aes(x=parch)) +
  geom_bar(fill = 'salmon', alpha=0.6) +
  facet_wrap(~surv, ncol=2)

6. ส่องตัวสุดท้ายเป็นเรื่อง สถานะการรอดชีวิต (0 = ไม่รอด, 1 = รอด) จะเห็นว่าอัตราการเสียชีวิตสูงกว่าบุคคลที่รอดชีวิต

# bar:: survived
titanic_df %>% 
  ggplot(mapping = aes(x=survived)) +
  geom_bar(fill = 'salmon', alpha=0.6)

เมื่อลองดูแต่ละตัวแปรแนวโน้มเบนไปทางไม่รอดชีวิตจะเหตุการณ์นี้ซะเยอะเลย แล้วตัวแปรไหนกันนะที่ส่งผลต่อการรอดชีวิตจริงๆ ก็เลยทดสอบทุกตัวแปรเข้าไปในโมเดล เพื่อดูว่าตัวแปรใดที่ Significant บ้าง (อย่าลืม Drop ตัวแปล surv ทิ้งไปก่อนนะครับ เดี๋ยวเพี้ยน) จะพบว่า ….

# Drop Label Variable titanic_df$surv
titanic_df$surv <- NULL

# Test Multivalidate
model_all <- glm(survived ~ ., data = titanic_df, family = 'binomial')
summary(model_all)
  • มีตัวแปร 2 ตัวที่มีค่า p-value มากกว่า 0.05 เราจึงจะพิจารณา Drop ทิ้งไป คือ parch, fare
  • ตัวแปรที่เราจะเอาไปใช้ในโมเดล คือ pclass, sex, age, sibsp
# Feature Selection:: Drop Not use Cols
titanic$parch <- NULL
titanic$fare <- NULL

🚂 Train Model: พี่จะสอนน้องเอง

เข้าสู่ขั้นตอนการเทรนแล้ว โดยเราต้องแบ่ง Train Dataset กับ Test Dataset โดยใช้โค้ดประมาณนี้นะครับ

# Split data 80:20
set.seed(137)
n <- nrow(titanic)
id <- sample(1:n, size = n*0.8)
train_df <- titanic[id, ]
test_df <- titanic[-id, ]

ซึ่งเราใช้ฟังก์ชัน sample ในการแบ่ง dataset ออกเป็น 80% เป็น Train dataset และอีก 20% เป็น Test dataset

# train model
logit_model <- glm(survived ~ pclass + sex + age + sibsp, data = train_df, 
										family = "binomial")
p_train <- predict(logit_model, type = "response") # probability %

train_df$p_train <- p_train
train_df$pred <- if_else(p_train >= 0.5, 1, 0)
train_df$pred_name <- if_else(p_train >= 0.5, "Survived", "Died")
tail(train_df, 10)
  • ในสมการของเราจะได้ survived เป็นฟังก์ชันของ pclass + sex + age + sibsp
  • family = ‘binomial’ ใช้สำหรับ logistic regression ที่ต้องการผลลัพธ์เป็น 2 ทาง (Survived, Died)
  • ใน predict เราระบุ type = “response” เพื่อต้องการผลลัพธ์เป็น % Probability และเก็บผลลัพธ์ที่ได้เข้าใน titanic_df$p_train
  • เพื่อให้อ่านผล % Prob. ง่ายขึ้นจึงแปลงให้เป็น Label โดยเงื่อนไข if_else(p_train >= 0.5, 1, 0) และ if_else(p_train >= 0.5, “Survived”, “Died”)
  • เมื่อลองแสดงผลลัพธ์ออกมา จะได้ … (ทายถูกเยอะอยู่นะเนี่ยย 😝)

มาลองทดสอบ accuracy ของโมเดลกัน

## confusion matrix table() cross
conf <- table(train_df$pred, train_df$survived, 
              dnn=c('predicted', 'actual'))
conf

## evaluation
accu <- (conf[1,1] + conf[2,2]) / sum(conf)
precision <- conf[2,2] /  (conf[2,1] + conf[2,2])
recall <- conf[2,2] / (conf[1,2] + conf[2,2])
f1score <- 2 *((precision * recall) / (precision + recall))

cat('Accuracy : ', accu,
    '\nPrecision : ', precision,
    '\nRecall : ', recall,
    '\nF1 Score : ', f1score)
  • เราเอามาสร้างเป็นตาราง Confusion Matrix และคำนวณ ..
  • Accuracy: ไม่ว่าผลลัพธ์จะเป็น Survived หรือ Died แล้วโมเดลทายถูก / จำนวนทั้งหมด
  • Precision: เมื่อเรา Predicted ว่า Survived → เราทายถูกกี่ครั้ง / จำนวนที่เราทายว่า Survived ทั้งหมด
  • Recall: เมื่อข้อมูล Actual เป็น Survived → เราทายถูกกี่ครั้ง / จำนวนที่ข้อมูล Actual เป็น Survived ทั้งหมด
  • F1 Score: วัดค่าผลลัพธ์โดยรวมของโมเดล
  • พี่ว่าพี่โอเคกับผลลัพธ์แล้วนะ ทีนี้ไปลองกับ Test Dataset กัน

🧪 Test Model: ลองดูก่อน

เราจะใช้ test_df ที่ได้แบ่งไว้ก่อนหน้านี้ ในการทดสอบโมเดล

# test model
p_test <- predict(logit_model, newdata = test_df, type = "response") # probability %
test_df$p_test <- p_test
test_df$pred <- if_else(p_test >= 0.5, 1, 0)
test_df$pred_name <- if_else(p_test >= 0.5, "Survived", "Died")
tail(test_df, 10)
## confusion matrix table() cross
conf <- table(test_df$pred, test_df$survived, 
              dnn=c('predicted', 'actual'))
conf

## evaluation
accu <- (conf[1,1] + conf[2,2]) / sum(conf)
precision <- conf[2,2] /  (conf[2,1] + conf[2,2])
recall <- conf[2,2] / (conf[1,2] + conf[2,2])
f1score <- 2 *((precision * recall) / (precision + recall))

cat('Accuracy : ', accu,
    '\nPrecision : ', precision,
    '\nRecall : ', recall,
    '\nF1 Score : ', f1score)
  • จะเห็นว่าค่า Accuracy เพิ่มขึ้นมาจาก Train dataset อาจเป็นเพราะขนาดของ Test dataset มีขนาดเล็กมาก ค่า Accuracy อาจมีความผันผวน หากจำนวนของ Test dataset เพิ่มมากขึ้นอาจจะทำให้โมเดลทายผิดเพิ่มขึ้นก็ได้ ไม่ก็ dataset ที่ใช้ทดสอบนั้นง่ายเกินไป ฮ่าๆๆ
  • แต่เรามั่นใจได้ว่ามีการแบ่ง Train & Test โดยวิธีการ Random sampling แล้ว

🍌มา Role play แบบเรียลไทม์กัน

จากค่า coefficient ที่ได้จาก Train model จะสามารถเขียนสมการได้ ดังนี้

  • logit(p) = 3.07026 – 1.29951 * pclass + 2.44141 * sex – 0.04553 * age – 0.37714 * sibsp
  • ลองกำหนดค่าตัวแปรให้โมเดลลองทำนายดู เช่น ผู้โดยสารชั้น 2 (pclass_ = 2) เป็นผู้ชาย (sex_ = 0) อายุประมาณ 47 ปี (age_ = 2) และมีมากับคู่สมรส 1 คน (sibsp_ = 1)
coefs <- coef(logit_model)

pclass_ <- 2
sex_ <- 0
age_ <- 47
sibsp_ <- 1

# Calc Realtime
survived_pred <- coefs[[1]] + coefs[[2]]*pclass_ + coefs[[3]]*sex_ + coefs[[4]]*age_ + coefs[[4]]*sibsp_ 
survived_pred

# Calc Realtime -> Prop
survived_prop <- exp(survived_pred) / (1 + exp(survived_pred))
survived_prop

# Calc Realtime -> Prop -> Survived?
is_survived <- if_else(survived_prop >= 0.5, 1, 0)
is_survived

# Calc Realtime -> Prop -> SurvivedName?
is_survived_name <- if_else(survived_prop >= 0.5, "Survived", "Died")
is_survived_name

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

ไปเล่นได้ที่ลิงก์นี้ค้าบบ: Data Modeling DSB Batch#11


จบแล้วครับ สำหรับการสร้างโมเดล Logistic Regression แบบง่ายๆ เพิ่มเสริมความเข้าใจหลังจากที่เข้าเรียนมาอย่างหนัก 😭 ครั้งหน้าจะเป็นโมเดลไหน ทำยังไง สามารถกดติดตามข่าวสารรอได้เลยนะครับ 🤩



One response to “พาลง Titanic ด้วย Logistic Regression”

  1. […] ขึ้นมา (อีกแล้ว เหอะๆๆ 😝 หลังจากที่พาลง titanic shiny app […]

Leave a Reply

Your email address will not be published. Required fields are marked *

Search

About

Feasible เว็บไซต์ที่นำเสนออาชีพปัจจุบันที่เรา (เจ้าของเว็บ) กำลังทำ ไม่ว่าจะเป็น นักวิเคราะห์ข้อมูล นักเรียน นักอ่าน นักฟาร์ม และอีกหลากหลายมุมมอง เรียกได้ว่าเป็น ‘แกงโฮะ’ เลยล่ะ ฮ่าๆๆ ติดตาม Content ที่จะทำออกมาได้เรื่อยๆ นะครับ ขอบคุณที่เข้ามาเยี่ยมกัน 😁✌️

Social Icons

Buy Me a Coffee

😁 ขอบคุณทุกน้ำใจ ทุกการสนับสนุนครับ 👏