ฝึกใช้งาน apply family ฟังชั่น for loop ยอดนิยมใน R

การเขียน for loop ใน R อาจจะดูยุ่งยากและมีปัญหาเรื่อง performance i.e. ความเร็วในการรันโปรแกรมจะช้ากว่าการเขียนโค้ดแบบอื่นๆ วันนี้เราจะอธิบายฟังชั่น apply() และเทคนิคที่จะช่วยให้โปรแกรมรันเร็วขึ้นแบบไม่ต้องเขียน for loop เลย

ถ้าใครยังไม่ว่า for loop ปกติเขียนยังไง ลองอ่านบทความ control flow ของเราก่อน ข้อมูลที่ใช้สอนใน tutorial นี้ชื่อ mtcars เราสามารถโหลด data frame ใน RStudio ด้วยโค้ดนี้

data("mtcars") 
head(mtcars) 

Regular For Loop

สมมติถ้าเราต้องการหา column_mean ของแต่ละคอลั่มใน data frame การเขียน for loop ปกติจะค่อนข้างยุ่งยาก เขียนโค้ดอย่างน้อย 4-5 lines

# write for loop to compute column means
for(i in seq_along(mtcars)){
  column_name <- colnames(mtcars)[i]
  column_mean <- round(mean(mtcars[,i]), 2)
  print(paste0(column_name, ": ", column_mean))
}

เราใช้ฟังชั่น seq_along() สร้าง numeric vector 1:11 ความยาวเท่ากับจำนวนคอลั่มของ mtcars เสร็จแล้วเขียน for loop ดึงแต่ละคอลั่มออกมา mtcars[,i] เพื่อหาค่าเฉลี่ยและแสดงผลใน console ด้วยฟังชั่น print()

มีวิธีไหนใน R ที่จะช่วยให้เราได้คำตอบเดียวกัน i.e. column_mean แต่เขียนโค้ดสั้นและเข้าใจง่ายกว่านี้ไหม?

Apply Save Life!

ตอบเลยว่ามี !! และฟังชั่นที่ R programmer นิยมใช้แทน for loop คือ apply() ปกติเราจะใช้ apply กับ data structures แบบ 2-dimension เช่น matrix (m x n) และ data frame

margin=1 คือการ apply function แบบ row-wise | margin=2 เป็นแบบ column-wise

apply() มีสาม required arguments ประกอบด้วย

  • X = matrix หรือ data frame
  • MARGIN = ตัวเลข 1, 2 โดยที่ 1 คือ row-wise และ 2 คือ column-wise
  • FUN = function ที่เราต้องการใช้งาน เช่น ฟังชั่นสถิติ mean sum sd

ลองรันโค้ดด้านล่างใน RStudio และวิเคราะห์ผลลัพธ์ที่ได้

# find row means
apply(mtcars, 1, mean)

# find column means - same as for loop code we write above
apply(mtcars, 2, mean)

# see more examples 
example(apply)

ประโยชน์ของ apply() ช่วยให้โค้ดเราอ่านง่ายขึ้น โปรแกรมรันได้เร็วขึ้นเมื่อเทียบกับการเขียน for loop ปกติ Tip – R มีฟังชั่น colMeans() colSums() rowMeans() และ rowSums() ที่ใช้กับ data frame ได้เลย

Another Apply

ฟังชั่นในตระกูล apply มีอีกหลายตัว แต่วันนี้เราจะอธิบายตัวที่ใช้บ่อยๆนั่นคือ lapply() และ sapply() ทั้งสองแบบใช้เหมือนกัน แต่ sapply() จะพยายาม simplify output ให้ดูง่ายขึ้นใน console

ทั้งสองฟังชั่นมี required arguments สองตัวคือ X และ FUN

# X = mtcars, FUN = mean
# lapply computes column means
lapply(mtcars, mean)

# sapply computes column means
sapply(mtcars, mean)

lapply() จะได้ผลลัพธ์ออกมาเป็น list แต่ sapply() จะเปลี่ยนผลลัพธ์เป็น vector จะใช้ฟังชั่นไหนขึ้นอยู่กับสถานการณ์และรูปแบบ data structure ที่ต้องการเอาไปใช้งานต่อ

Anonymous Function

ขอบคุณรูปสวยๆจาก Unsplash

Anonymous Function คือฟังชั่นไร้ชื่อที่เราเขียนขึ้นมาเร็วๆภายในฟังชั่นอื่นๆ เทคนิคนี้มีประโยชน์มากเวลาใช้กับ apply() ลอง copy โค้ดด้านล่างไปรันใน RStudio และวิเคราะห์ผลลัพธ์ที่ได้

# is it whole number?
apply(mtcars, 2, function(x) all(x == floor(x)) )

# standardization 
apply(mtcars, 2, function(x) (x - mean(x)) / sd(x))

# feature scaling 
apply(mtcars, 2, function(x) (x - min(x)) / (max(x) - min(x)))

argument ที่สามใน apply() เราเขียนฟังชั่นขึ้นมาเองด้วย template นี้ function(x) do something ...

anonymous function แรกที่เราเขียนใช้ดูว่าคอลั่มไหนบ้างที่เป็นตัวเลขจำนวนเต็ม i.e. ไม่มีจุดทศนิยม ผลลัพธ์จะออกมาเป็นเวคเตอร์ TRUE FALSE ส่วนฟังชั่นสองและสามคือเทคนิคการ normalize ข้อมูลของเราก่อนเทรนโมเดล ใช้ได้กับข้อมูลแบบ numeric เท่านั้น

Key Takeaway

  • โดยทั่วไป R programmer จะพยายามเลี่ยง for loop ถ้าไม่จำเป็น
  • ฝึกใช้ apply functions จะช่วยให้โค้ดเราอ่านง่ายขึ้น และทำงานเร็วขึ้นกว่า for loop นิดหน่อย
  • R มี package purrr ของ Hadley Wickham ที่ออกแบบมาสำหรับงาน for loop โดยเฉพาะ ลองดูตัวอย่างด้านล่าง เรารัน kmeans แบบ 2-5 clusters แล้วดึงค่า tot.within ss ออกมา plot กราฟ
# example of kmeans 2-5 segments
library(purrr)
set.seed(99)
results <- map_dbl(2:5, ~kmeans(iris[,1:4], centers = .x)$tot.within)
plot(2:5, results, type = "b", 
     xlab = "number of clusters", ylab = "total within ss"))
จากกราฟ เราแนะนำว่าควรมี 3 clusters ดูจากจุดที่ tot.wthin ตกลงมาเยอะ (iris มีสาม species)

References

Leave a Reply