Case Study: Tips 數據集 Speedrun

Bananish
The whispers of a data analyst
15 min readMar 12, 2023

2022 開始在東吳上資料科學碩士在職專班的課程
這是來自多變量分析課程中老師用來介紹 rcommander GUI 的範例
其實,用按的並不比直接寫還快~

案例介紹

被收錄在 Practical data analysis : case studies in business statistics 中的 Tips 數據集中記錄了每個餐會的小費、帳單等資訊。

一位服務生記錄了他在一家餐廳工作的幾個月間,收到的小費與相關的訊息

這個研究的目標在於探索小費多寡與其他因素之間的關係。

其中的資訊包含:

  • tip in dollars
  • bill in dollars
  • sex of the bill payer
  • whether there were smokers in the party
  • day of the week
  • time of day
  • size of the party

Step 1:引入資源

主要使用資源

  1. data.tabledplyr 用於資料的取用與整理
  2. ggplot2plotly 用於繪圖
  3. abn 用於結構探索
library(data.table)
library(dplyr)
library(ggplot2)
library(plotly)
library(abn)

次要使用的資源

未被在一開始載入至環境中會零散的出現在各個角落

  1. skimr 摘要與描述資料集
  2. scales 格式化或美化數字的呈現
  3. colorspace 調整 ggplot2 圖中的顏色
  4. ggthemes 調整 ggplot2 圖中的版型
  5. forcats 處理 factor
  6. jtools 展示回歸分析的結果
  7. mediation 評估中介效應

Step 2:資料獲取

var_path <- "data/20230311_tips.csv"

data <- var_path %>% fread()

Step 3:預整理

文獻多、資料定義清楚,等先備資訊充足時可以馬上開始整理。

如果先備資訊不足可能會先進行探索在整理。

data <- data %>% mutate(
sex = sex %>% factor(., c("Female", "Male")),
smoker = smoker %>% factor(., c("No", "Yes")),
day = day %>% factor(., c("Thur", "Fri", "Sat", "Sun")),
time = time %>% factor(., c("Lunch", "Dinner"))
)

Step 4:資料探索

描述性統計

此階段注意的重點包含:

  1. 資料大小
  2. 資料正確性,是否有奇怪的資訊
  3. 類別與連續變項有哪些、大概的分佈
  4. 類別變量有沒有可能要重新分組
  5. 連續變量是否有需要轉換以符合常態分佈,顯然有幾個可能需要
data %>% skimr::skim()
tips 資料集概述
tips 資料集 factor 欄位概述
tips 資料集 numeric 欄位概述

可視化

以 3D 散點圖表示三個連續變項 tip、bill、size 之間的關係

data %>%
plot_ly(x = ~size, y = ~total_bill, z = ~tip, type = "scatter3d", mode = "markers
3D 散點圖: tip、bill、size 間的關係

用 plotly 可以旋轉 3D 圖,顯然是有某種相關

以 3D 散點圖表示三個連續變項 tip、bill、size 之間的關係,並加上以顏色表示是否為 smoker

data %>%
plot_ly(x = ~size, y = ~total_bill, z = ~tip, color = ~smoker, type = "scatter3d", mode = "markers")
3D 散點圖: tip、bill、size 間的關係,以及是否為 smoker

smoker 可能不會對三個連續變量的關係有所影響

更複雜一點,想知道不同 day、smoker、sex 中 total_bill 與 tip 的關係

在 R 中用 ggplot 做 sub plot 好像比較快一點,調整顏色與版型也比較快

ggplotly(
data %>%
mutate(
day = day %>% paste("day =", .),
smoker = smoker %>% paste("smoker =", .)
) %>%
ggplot(aes(x = total_bill, y = tip, color = sex)) +
geom_smooth(method = "lm", se = F, size = .5, linetype = 2, formula = "y ~ x") +
geom_point(alpha = .8) +
facet_wrap(smoker ~ day, ncol = 4, scales = "free") +
scale_x_continuous(labels = scales::label_comma()) +
scale_y_continuous(labels = scales::label_comma()) +
colorspace::scale_color_discrete_diverging("Green-Orange") +
colorspace::scale_fill_discrete_diverging("Green-Orange") +
ggthemes::theme_fivethirtyeight() +
theme(legend.position = "bottom")
)
散點圖 + 線性趨勢: tip、bill 間的關係在不同 smoker、day、sex 組合中的關係的差異

在這樣多層的狀況下,少數的 smoker X day 的組合好像有顯示出 Female、Male 的 bill vs tips 的斜率差異,但可能要檢定才會知道

用性別畫個盒狀圖看看

ggplotly(data %>%
mutate(
day = day %>% paste("day =", .),
smoker = smoker %>% paste("smoker =", .)
) %>%
ggplot(aes(x = sex, y = tip, color = sex)) +
geom_boxplot() +
facet_wrap(~day, ncol = 4, scales = "free") +
scale_y_continuous(labels = scales::label_comma()) +
colorspace::scale_color_discrete_diverging("Green-Orange") +
colorspace::scale_fill_discrete_diverging("Green-Orange") +
ggthemes::theme_fivethirtyeight() +
theme(legend.position = "bottom"))
盒狀圖:不同 sex、day 的 tip 盒狀圖

好像也很難說不同性別(結帳的人)tips 有什麼不一樣

Step 5:假設檢定

研究的環境限制

因為沒有這個主題(bills vs tips)的 data context、historical conclusion。

此外,這顯然是一個類似 Cohort study 的觀察性研究(exposure:bills、outcome:tip)。

所以很難確定:

  1. Bias:有沒有錯誤分類的可能、有沒有可能這個服務生都被分配到某種特徵的客人、有沒有可能這是一間沒有代表性的餐廳
  2. Confounder:不太確定實際 casual DAG 長什麼樣子,但很理性的可以想像 bills 應該某種程度會影響 tips,這也是要檢查的主要項目,但其他的變因會不會對這兩者有影響而成為混淆因子、又或者是有沒有其他沒被觀察的混淆,例如吃飯的人今天心情與身體好不好、對這間店的態度如何

所以,不管怎麼做,這篇研究就算可以屏除 random error 的影響,在資訊很不充足的情形之下,對魚研究最後發現所謂有顯著的成果,可能都很難上升到因果效應的這種層次。也就是在研就的內部有效性上應該存在著許多未解之謎。

結構探索

既然,都不太確定 DAG 是什麼,所以用了 abn 中的方法自動找到合理的 DAG。

此時,把 day 進行重新分組,bill、tip 進行 log transformation

data_abn <- data %>%
mutate(day = forcats::fct_recode(day, "Weekday" = "Thur", "Weekday" = "Fri", "Weekend" = "Sat", "Weekend" = "Sun")) %>%
mutate_at(.vars = c("total_bill", "tip"), ~ log(.)) %>%
data.frame()

data.dists <- list(
"total_bill" = "gaussian",
"tip" = "gaussian",
"sex" = "binomial",
"smoker" = "binomial",
"day" = "binomial",
"time" = "binomial",
"size" = "gaussian"
)


dag.banned <- matrix(0, ncol(data_abn), ncol(data_abn), dimnames = list(names(data.dists), names(data.dists)))

dag.banned["size", "total_bill"] <- 1
dag.banned["total_bill", "tip"] <- 1

data_abn %>%
buildScoreCache(data.df = ., data.dists = data.dists, max.parents = 1, dag.banned = dag.banned) %>%
mostProbable() %>%
fitAbn() %>%
plot()

透過比較不同的 max.parent 設置下,可以找到 mlik 比較優秀的 max.parent 參數,這邊設定為 1(沒有寫在上面)。

另外,原則上常規的決策路徑中:

  1. bill 影響 size:像是因為不想付太多錢所以不要找太多人
  2. tip 影響 bill:像是不想付太多小費所以不要點太多

這兩種因果路徑可能都有一點合理又有一點不合理,但不像是常態,所以用矩陣 dag.banned 確保這個路徑被移除在外。

Causal DAG 路徑探索的成果

這是最後 abn 判斷認為比較合理的 causal DAG。

性別決定哪天吃飯(似乎可以接受)、選擇哪天吃飯後決定中午還是晚上(免搶接受)

smoker 被晾在一旁,表示跟其他變量沒關係,跟前面看到的差不多

size 透過中介 bill 進而影響 tip,感覺這個好像非常合理

越多人吃飯總金額越高、總金額越高對 tip 影響越大

回歸分析

雖然已經有路徑係數了,但想再用回歸檢查一次

主要想檢查的是:

  1. 透過可視化跟 abn ,已經對 sex、time、day、smoker 沒啥興趣,原則上應該檢查
  2. 但應該先確定 size、bill、tip 的 chain 關係
list_model <- c("total_bill", "size", "total_bill + size") %>%
paste("tip ~", .) %>%
c(., "total_bill ~ size") %>%
lapply(., as.formula) %>%
lapply(., function(x) {
glm(data = data_abn, formula = x)
})

suppressMessages({
list_model %>%
jtools::export_summs(.,
error_format = "[{conf.low}, {conf.high}]",
model.names = c("total bill", "size", "both", "size by total bill")
)
})
  1. tip ~ bill:有顯著差異,r2 也不差,把 r2、ai、bic 先當一個標準看其他的模型
  2. tip ~ size:r2、aic、bic 好像表現在比 model (1) 差一些
  3. tip ~ size + bill:如果有一個是中介,原則上期待兩個變量的 variance 應該會有一個變差一些,畢竟兩個相依,確實從 size 的 se 或 p 上可以看到類似的反應。r2 只有微乎其微的增加一點,表示多加另一個變量對模型的解釋力沒有太多幫助,雖然 aic 變少但 bic 變多;這些都是存在中介可能會引發的徵兆(但也有可能不是)
  4. bill ~ size:如預期的關聯

順便看了殘差

par(mfrow = c(2, 2))
list_model[[1]] %>% plot()

par(mfrow = c(2, 2))
list_model[[3]] %>% plot()

勉強還行(殘差散佈沒有太明顯詭譎的趨勢)

中介分析

其實差不多意思,但可以試一次

model_mediate = mediation::mediate(model.m = list_model[[4]], model.y = list_model[[3]],
treat = "size", mediator = "total_bill")
model_mediate%>%summary
model_mediate%>%plot

--

--