Data Sci Boot Camp Batch#11 : R Data Transformation

พื้นฐาน R สามารถอ่าน Blog ตอนก่อนหน้าได้น้า

สำหรับวันนี้แอดทอยมา Recap ที่สอน Data Transformation มีตามนี้น้า

Glue Package

Glue เป็น package ที่ทำหน้าต่อต่อคำ แบบเดียวกับ paste หรือ paste0

  • install package + load
install.packages("glue")
library(glue) # include lib ก่อนใช้งาน
  • sample use - ผมชอบแบบนี้นะ เหมือน C# พอดีเลย
# string template
my_name <- "Pingkunga"
my_age <- 34

# String template in base R is paste 0
text <- glue("Hi! my name is {my_name} age about {my_age}")
print(text)
#

ผมชอบแบบนี้นะ เหมือน C# String interpolation แบบ $"Hello, {name}! Today is {date.DayOfWeek}, it's {date:HH:mm} now."

Recap Data Frame Func

  • mtcars - build in DataSet ใน R
  • View(YOUR_DataFrame_Name) - แสดงข้อมูลในรูปแบบตาราง หลังจากนี้ใช้ mtcars
  • head(mtcars, 5) - ดึงข้อมูลมา 5 แถวแรก
  • tail(mtcars, 5) - ดึงข้อมูลมา 5 แถวท้าย
  • glimpse(mtcars) และ str(mtcars) ดูโครงสร้างของ Data Frame
    - str มาจาก base r
    - glimpse มาจาก dplyr มาแก้ปัญหาการแสดงผลของ str
  • summary(mtcars) แสดง basic stat Min / 1st Quartile / Median / Mean / 3rd Quartile / Max ของแต่ละ Column
  • names(mtcars) / colnames(mtcars) - List Column in df
  • rownames(mtcars) - เพิ่งรู้ว่ามีแบบนี้ด้วย ชื่อแถว อารมณ์แบบ pk ใน table

best practice: move rownames to col name จะได้ manage สะดวก

# best practice move rownames to col name
mtcars$model
mtcars$model <- rownames(mtcars)

# remove rowname
rownames(mtcars) <- NULL
View(mtcars)

dplyr - data transformation

- Data Pipeline

เราสามารถให้ output ของแต่ละคำสั่ง มันส่งต่อกันไปได้เรื่อยๆ เหมือนสายก่ารผลิด โดยจะเรียกว่า Pipeline

  • เดิม - ดึง column 1-5 จาก df mtcars
select(mtcars,1:5)
  • Pipeline มี 2 แบบ %>% or |> (new Style)
mtcars %>%
   select(1:5) 

#นอกจากนี้ทำ Chain ต่อได้ ดึง column 1-5 จาก df mtcars จากนั้นกรอง 5 แถวแรกขึ้นมา
# - แบบ %>%
mtcars %>%
  select(1:5)  %>%
  head()

# - แบบ |>
mtcars |>
  select(1:5)  |>
  head()
- select

ดึงข้อมูลออกมาจาก Data Frame โดยมีหลายแบบ อารมณ์เหมือน SQL SELECT

  • ระบุ Id ของ Column
# เอา Column ที่ 1 - 5
mtcars |>
  select(1:5)

# เอา Column ที่ 2
mtcars |>
  select(2)

# เอา Column ที่ 2 / 10
mtcars |>
  select(2,10)
  • ระบุ ชื่อ Column
mtcars |>
  select(model)

mtcars |>
  select(am, hp, wt, model)
  • ผสม
# move model to left size
select(mtcars, model, 1:5)

# Pipeline Style
mtcars |>
  select(model, 1:5)
  • เอา helper verb มาช่วย
# 1. everything() เอา Column ที่เหลือที่ไม้ได้ระบุตอน Select มาต่อได้ เคสนี้ ระบุ model และให้เอาที่เหลือมาต่อท้าย
mtcars |>
  select(model, everything())

# 2. starts_with หา Column ที่ชื่อต้นทางที่ระบุ
mtcars |>
  select(starts_with("h"))

# 3. ends_with หา Column ที่ลงท้าย
mtcars |>
  select(ends_with("p"))

# 4. contains หา Column ที่มีตัวอักษร
mtcars |>
  select(contains("a"))
- filter
  • Concept เหมือน SQL / Google Sheet ตัวอย่างการใช้งานตามนี้เลย
# Simple Condition 
m |> 
  filter(mpg > 30)
m |> 
  filter(mpg < 15)

# and &
m |> 
  select(1:5) |>
  filter(hp < 100 & disp < 100)

# or |
m |> 
  select(1:5) |>
  filter(hp < 100 | disp < 100)

# หลายเงื่อนไข เคส AND &
m |> 
  filter(mpg < 15 & hp > 240)

# หลายเงื่อนไข เคส AND & + SELECT
m |> 
  select(model, mpg, hp, wt) |>
  filter(mpg < 15 & hp > 240 & wt > 3.8) 
  • With RegEx เอา Regular Expression มาช่วย
    - grep(YOUR_RegEx, TEST_VECTOR) - return vector ของ index ที่พบ
    - grepl(YOUR_RegEx, TEST_VECTOR) - return boolean vector TRUE พบ กลับกัน FALSE
# recap RegEx
state.name # build in dataset

# ======================================
# grep sample
# ดูว่ามี state ไหนที่ขึ้นต้นด้วย A
grep("^A", state.name)  
#[1] 1 2 3 4

# ลองดึงชื่อ state จริงๆ
state.name[1:4]

# ดูว่ามี state ไหนทีลงท้ายด้วย A
grep("a$", state.name)
#ลองดึงชื่อออกมา
state.name[grep("a$", state.name)]

# ======================================
# grepl sample
# return true / false
grepl("^A", state.name)  

state.name[grepl("^A", state.name)]
  • เอา Grep ไปใช้กับ Filter
# Start with Merc*
m |> 
  filter(grepl("^Merc", model)) %>%
  select(model, am, wt, hp)

# start with H or T or P
m |> 
  filter(grepl("^[HTP]", model)) %>%
  select(model, am, wt, hp)

# not start with H or T or P
m |> 
  filter(grepl("^[^HTP]", model)) %>%
  select(model, am, wt, hp)
- mutate (Create New Column
  • สร้าง Column ใหม่ จาก hp มาเป็น hp_double
# จาก hp มาเป็น hp_double
m|>
  select(model, hp) |>
  mutate(hp_double = hp * 2) |>
  head()

จาก hp มาเป็น hp_double / hp_addfive  ต่อๆกัน
m|>
  select(model, hp) |>
  mutate(hp_double = hp * 2, 
         hp_half = hp /2, 
         hp_addfive = hp + 5) |>
  head()
  • นอกจากนี้ เอา Column ที่สร้างมาใช่ต่อได้ ตัวอย่างเอา hp_half มาใช้ต่อ hp_addfive / hp_per_wt
m|>
  select(model, hp, wt) |>
  mutate(hp_double = hp * 2, 
         hp_half = hp /2, 
         hp_addfive = hp_half + 5, 
         hp_per_wt = hp_half/wt) |>
  head()
  • ถ้าต้องการเติมข้อมูลทุก Columnให้เหมือน (R - Recycle or Python broadcast) ใช้ Random()
m|>
  select(model, hp) |>
  mutate(hp_double = hp * 2, 
         hp_half = hp /2, 
         hp_addfive = hp + 5,
         # recycle or broadcast in python fill row
         random = "fill_your_word") |>
  head()
  • ใช้ if_else / case_when มาจัดกลุ่มข้อมูล ทำ Segment
# if-else create segment column
# ifelse - classic base r
# if_else - modern r perf improve

m |> 
  select(model, hp, wt) |>
  mutate(segment = if_else(hp > 200, "High", "Low")) |>
  mutate(segment = case_when(
    hp > 200 ~ "high",
    hp > 100 ~ "medium",
    ## else
    TRUE ~ "low"
  ))

m |> 
  select(model, hp, wt) |>
  mutate(segment = if_else(hp > 200, "High", "Low")) |>
  mutate(segment = case_when(
    hp > 200 ~ "high",
    hp > 100 ~ "medium",
    hp <= 100 ~ "low"
  ))
- arrange - sort data เหมือน SQL Order By

ปกติจะเรียงจากน้อยไปมาก ถ้าต้องการจากมากไปน้อยใช้ desc()

  • Single Column
# single Column
m |>
  select(model, hp, wt) |>
  arrange(model)

m |>
  select(model, hp, wt) |>
  arrange(desc(model))
  • multi-column sort
# multi-column sort
m |>
  select(model, hp, wt, am) |>
  arrange(am ,hp)

m |>
  select(model, hp, wt, am) |>
  arrange(am , desc(hp))
- summarise สรุปข้อมูลทางสถิติ
m |>
  select(model, am, hp) |>
  mutate(am = if_else(am==0, "Auto", "Manual")) |> 
  summarise(avp_hp = mean(hp),
            sum_hp = sum(hp),
            med_hp = median(hp),
            sd_hp = sd(hp),
            var_hp = var(hp),
            n = n())

# result
   avp_hp   sum_hp  med_hp  sd_hp    var_hp   n
1 146.6875   4694    123    68.56287 4700.867 32
  • ถ้าต้องการแบ่งกลุ่มใช้ Group By
m |>
  select(model, am, hp) |>
  mutate(am = if_else(am==0, "Auto", "Manual")) |> 
  group_by(am) |> 
  summarise(avp_hp = mean(hp),
            sum_hp = sum(hp),
            med_hp = median(hp),
            sd_hp = sd(hp),
            var_hp = var(hp),
            n = n())
# result
# A tibble: 2 × 7
  am     avp_hp sum_hp med_hp sd_hp var_hp     n
  <chr>   <dbl>  <dbl>  <dbl> <dbl>  <dbl> <int>
1 Auto     160.   3045    175  53.9  2906.    19
2 Manual   127.   1649    109  84.1  7066.    13
- Function อื่นๆ (sample / slice / pull / rename)
  • sample() สุ่มข้อมูลขึ้นมา จากเดิมมี head() / tail() อันนี้ช่ววยให้เลือกได้ตัวแทนได้กระจายขึ้น
# sample
m |>
  sample_n(5)  # sample random

m |>
  sample_frac(0.5)  # sample random 50%
  • slice() แบ่งข้อมูลจาก Data Frame
# เดิมๆใน df
m[c(1,3,5),]

# Slice 
m |>
  slice(1:5)
#เลือกบาง Column ใส่ Vector เข้าไป
m |>
  slice(c(1,3,5))
  • pull ดึง Column ออกจาก Data Frame จะได้เป็น Vector
# pull - return as vector
a = m |> 
     filter(hp > 200) |>
     pull(model)
class(a)
typeof(a)

# check is vector https://stackoverflow.com/questions/19501186/how-to-test-if-object-is-a-vector
is.atomic(a) # TRUE

# use model a Vector Element Name
m %>%
  filter(hp > 100) %>%
  pull(hp, model)
  • rename เปลี่ยนชื่อ Column จริงๆมันคล้ายกับ select
m |>
  select(model,
         horse_power = hp,
         tranmission = am,
         weight = wt) |>
  head()

# rename column model to car -- same as select
m |> 
  rename(car = model, weight = wt)

ใช้ SQL Query ข้อมูลจาก Data Frame

  • sqldf = lib ที่ช่วยใช้ SQL Query ข้อมูลจาก Data Frame
  • m = dataframe
install.packages("sqldf")
library(sqldf)

# SQL Case In-sensitive
sqldf("SELECT model, am , hp 
       FROM m")

sqldf("SELECT am , AVG(hp), SUM(hp) 
       FROM m
       GROUP BY am") 

How to handle missing value = NA

- NA คือ อะไร
  • NA ข้อมูลที่ไม่มีค่า โดยปัญหาของมัน ทำให้บางสูตรใช้งานไม่ได้
# missing value
nums <- c(1:10, NA, 11:15)
# why missing value cause problem
mean(nums)
# [1] NA
sum(nums)
# [1] NA
  • จริงๆ มันใช้งานได้แหละ แต่ต้องมาตัด NA ออก na.rm = TRUE rm = remove
# handle na
mean(nums, na.rm = TRUE)
sum(nums, na.rm = TRUE)
  • ตรวจสอบ Vector Contain และเอามาตัดของที่ไม่มี NA
# Check Vector Contain NA
nums[is.na(nums)]
# [1] NA

# Filter NA out
nums[!is.na(nums)]
# [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
nums2 <- nums[!is.na(nums)]
- think before filtering NA

อันนี้้แอดทอยยกตัวอย่างการ political preference USA ระหว่าง Democratic / Republican Party จาก 1000 คน มี 300 คนไม่ได้ตอบ มี 300 NA แล้วเรา Handle ยังไง ?

  1. do nothing > report 300 NA
  2. find the proxy column to ans (imputation) in NA Column such as like national healthcare (นโยบายของ Democratic) can be performed to democrat
  3. drop_na remove all rows //should <= 5% (arbitrary) / acceptable rate
- tidyr - package สำหรับ clean data
install.packages("tidyr") # if install tidyverse no run this 
library(tidyr)
- imputation

มีหลายวิธี ถ้าเอาง่ายๆ แอดทอยให้แนวทาง ดังนี้

  • Column ที่ NA เป็น Numeric ใช้ mean / median
# prepare data
id <- 1:5
friends <- c("ping", "tarn", "guide", "bank", "kook")
spending <- c(100, 150, NA, 200, 180)
service <- c("nf", "nf",NA,"amz", "nf")

# create df with NA
df <- data.frame(id,friends,spending, service)

# =========================================
# mean imputation replace NA with mean
avg_spending <- mean(df$spending, na.rm=T)
avg_spending

df2 |>
  mutate(spending = replace_na(spending, avg_spending))

# =========================================
# median imputation replace NA with median
median_spending <- median(df$spending, na.rm=T)

df3 |>
  mutate(spending = replace_na(spending, median_spending))
  • Column ที่ NA เป็น String ใช้ majority vote หรือ เอาแบบ Advance k-nearest neighbors
# majority vote
counts <- table(df$service)
max_value <- names(counts)[which.max(counts)]

df4 <- df |>
  mutate(service = replace_na(service, max_value))

wide vs long data format

จากฟังแอดทอยสอนมา

  • wide data format เป็นข้อมูลที่เหมาะสำหรับดูใน report เช่น ข้อมูลจำนวนประชากร แยกตามปี 2000, 2001 ... 2025 สังเกตุว่ามี Column ประเภทเดียวกันที่ซ้ำ ปี
  • long data format ผมมองการจัดข้อมูล Normalize Table แบบ 3NF ทุก Column เก็บข้อมูลไม่ซำซ้อนกัน

wide data format

|Country|2000|2001|....|2025|
|TH     |55M |56M |....|67M |
|USA    |350M|356M|....|400M |

long data format

|Country|YEAR|POPULATION|
|TH     |2000|  55M     |
|TH     |2001|  56M     |
...
|TH     |2025|  67M     |
|USA    |2000|  350M    |

Sample ใช้ dplyr - pivot_longer / pivot_wider

  • pivot_longer - wide > long
  • pivot_wider - long > wide
# sample wide data format
View(world_bank_pop)

# normalize to year + population
# dplyr + tidyr convert wide > long data format

world_bank_pop |>
  filter(indicator == "SP.URB.TOTL") |>
  View()

long_df = world_bank_pop |>
  filter(indicator == "SP.URB.TOTL") |>
  pivot_longer(cols = `2000`:`2017`,
               names_to = "year",
               values_to = "population")

# reverse operation pivot_wider
wide_df <- long_df |>
  pivot_wider(names_from = "year", 
              values_from = "population")

View(wide_df)

dplyr - JOIN Like SQL

  • มีเหมือนกัน แต่เราต้องดูข้อมูลก่อน
> band_members
# A tibble: 3 × 2
#  name  band   
# <chr> <chr>  
# 1 Mick  Stones 
# 2 John  Beatles
# 3 Paul  Beatles
> band_instruments
# A tibble: 3 × 2
#  name  plays 
#  <chr> <chr> 
# 1 John  guitar
# 2 Paul  bass  
# 3 Keith guitar
  • SQL - ความสัมพันธ์ของ 2 Data Source ใช้ Column name
SELECT *  
FROM band_members bm
  LEFT JOIN band_instruments bi
    ON bm.name = bi.name
  • ลอง Join ใน Column name มีเหมือนกัน
# pipe
band_members |>
  left_join(band_instruments, by = "name")

band_members |>
  inner_join(band_instruments, by = "name")
  • กรณีที่ไม่เหมือนกัน สร้าง Vector ของคู่อันดับ Column ที่สัมพันธ์ในที่นี่ pos_id กับ id
# case key name are difference
df_emp = data.frame(name= c("ping", "tarn", "kook"),
                    pos_id= c(1, 2, 1))
df_pos = data.frame(id=1:3,
                    name= c("DEV", "QA", "PM"))

df_emp |>
  inner_join(df_pos, by = c("pos_id"="id"))

# result
#   name.x pos_id name.y
# 1   ping      1    DEV
# 2   tarn      2     QA
# 3   kook      1    DEV

R with SQL

  • เลือก Lib ก่อน เพราะ DB แต่ละค่ายใช้ Lib ไม่เหมือนกัน เช่น .
    - SQLite - RSQLite
    - Postgres - RPostgreSQL
    - BigQuery - bigrquery
  • อ่านคู่มือการใช้งานแต่ละ Lib เช่น RPostgres ดูจากหน้าของ cran.r-project

Sample with SQLite ตัว DB ต้องอยู่ใน Working Directory เดียวกัน

# Set lib
install.packages("RSQLite")
library(RSQLite)

# bigrquery
getwd()
list.files()

# connect Database
conn <- dbConnect(SQLite(),"chinook.db")
conn

# list table
dbListTables(conn)
# list field
dbListFields(conn,"employees")

# send SQL to this database
dbGetQuery(conn, " SELECT FirstName, City, Country
                   FROM employees
                   WHERE City= 'Lethbridge'")

result <- dbGetQuery(conn, " SELECT FirstName, City, Country
                   FROM employees
                   WHERE City= 'Lethbridge'")

# write a new table from df
library(dplyr)
# SQL DROP
dbRemoveTable(conn, "models")
filter_mtcars <- mtcars |> 
  select(mpg, hp, wt) |>
  filter(hp > 200)
dbWriteTable(conn, "models", filter_mtcars)

dbGetQuery(conn, " SELECT * 
                   FROM models")

# close connection
dbDisconnect(conn)

# > conn
# <SQLiteConnection>
#  DISCONNECTED

# other db
# https://cran.r-project.org/web/packages/RPostgres/RPostgres.pdf

Homework - play with nycflights23 data with dyplyr


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts sent to your email.