haven 用來讀 SPSS 的
.sav。dplyr
用來挑欄位、改代碼、算次數。ggplot2
畫圖。knitr 的 kable()
把表排整齊。moments 算偏態和峰態。
Windows 的繪圖裝置不認得「微軟正黑體」這個名字,所以先用
windowsFonts() 登記成
JhengHei,圖上的中文才不會變方框。theme_set()
是把後面所有圖的底色、字級一次設好,不用每一張再寫一次。
library(haven)
library(dplyr)
library(ggplot2)
library(knitr)
library(moments)
if (.Platform$OS.type == "windows") {
windowsFonts(JhengHei = windowsFont("Microsoft JhengHei"))
base_family <- "JhengHei"
} else {
base_family <- ""
}
theme_set(
theme_minimal(base_size = 12, base_family = base_family) +
theme(
plot.title = element_text(face = "bold"),
panel.grid.minor = element_blank()
)
)
read_sav() 讀進
PISA_tawian2022_trimmed_lab.sav。%>%
是把左邊的結果交給右邊。transmute()
只留下這次要用的三欄,其他欄位不會留在 dat 裡。
性別在檔案裡是 1 和 2。case_when() 把 1 改成「女性」、2
改成「男性」,其他代碼(例如跳答)改成遺漏值。打工的 95、97、98、99
也是特殊代碼,不是真的打了那麼多次工,所以用 if_else() 改成
NA。這份檔案裡其實沒有這些代碼,還是先處理,避免以後被當成數字。
factor() 是把文字或數字變成類別,並用
levels 固定順序。性別固定先女後男;打工的 10 在題目裡是「10
次或以上」,圖的標籤寫成 10+,才不會被人看成剛好 10
次。
最後的 cat()
只是印出三個變項各有多少筆不是遺漏,用來確認資料有讀進來。
raw <- read_sav("PISA_tawian2022_trimmed_lab.sav")
dat <- raw %>%
transmute(
gender = case_when(
Gender == 1 ~ "女性",
Gender == 2 ~ "男性",
TRUE ~ NA_character_
),
workpay = if_else(WORKPAY %in% c(95, 97, 98, 99), NA_real_, WORKPAY),
pv1math = PV1MATH
) %>%
mutate(
gender = factor(gender, levels = c("女性", "男性")),
workpay_f = factor(
workpay,
levels = 0:10,
labels = c("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10+")
)
)
cat("有效樣本:性別", sum(!is.na(dat$gender)),
";打工", sum(!is.na(dat$workpay)),
";數學", sum(!is.na(dat$pv1math)), "\n")
## 有效樣本:性別 5599 ;打工 5599 ;數學 5599
性別只有兩類,所以先算每一類有幾人。count()
就是次數;後面的 mutate() 用該類人數除以總人數再乘
100,得到百分比。kable() 把這個表印出來。
圖用 geom_col(),因為高度就是表裡算好的人數,不是讓
ggplot 自己再數一次。geom_text()
把人數和百分比標在柱子上。scale_fill_manual()
指定兩種顏色。coord_cartesian(clip = "off")
是避免柱子頂端的文字被圖框切掉。
gender_tab <- dat %>%
filter(!is.na(gender)) %>%
count(gender, name = "人數") %>%
mutate(百分比 = round(人數 / sum(人數) * 100, 1))
kable(gender_tab, caption = "性別次數分配")
| gender | 人數 | 百分比 |
|---|---|---|
| 女性 | 2780 | 49.7 |
| 男性 | 2819 | 50.3 |
ggplot(gender_tab, aes(x = gender, y = 人數, fill = gender)) +
geom_col(width = 0.65, show.legend = FALSE) +
geom_text(
aes(label = sprintf("%d (%.1f%%)", 人數, 百分比)),
vjust = -0.3,
size = 3.6
) +
scale_fill_manual(values = c("女性" = "#4C78A8", "男性" = "#F58518")) +
scale_y_continuous(expand = expansion(mult = c(0, 0.22))) +
coord_cartesian(clip = "off") +
theme(plot.margin = margin(16, 10, 8, 8)) +
labs(
title = "學生性別人數",
x = "性別",
y = "人數"
)
表和圖的結果一樣:女性 2780 人(49.7%),男性 2819 人(50.3%),兩柱差不多高。
做法和性別同一套:count()
算每個次數有多少人,再算百分比。橫軸用前面做好的
workpay_f,所以 0 到 10+ 會照順序出現,不會因為某一格是 0
人就消失。這裡不用直方圖,因為答案只落在這幾個整數上,一柱對一個答案比較清楚。
work_tab <- dat %>%
filter(!is.na(workpay_f)) %>%
count(workpay_f, name = "人數") %>%
mutate(百分比 = round(人數 / sum(人數) * 100, 1))
kable(
work_tab,
col.names = c("每週打工次數", "人數", "百分比"),
caption = "每週打工次數(10+ 是 10 次或以上)"
)
| 每週打工次數 | 人數 | 百分比 |
|---|---|---|
| 0 | 4881 | 87.2 |
| 1 | 90 | 1.6 |
| 2 | 141 | 2.5 |
| 3 | 42 | 0.8 |
| 4 | 112 | 2.0 |
| 5 | 108 | 1.9 |
| 6 | 27 | 0.5 |
| 7 | 22 | 0.4 |
| 8 | 23 | 0.4 |
| 9 | 6 | 0.1 |
| 10+ | 147 | 2.6 |
ggplot(work_tab, aes(x = workpay_f, y = 人數)) +
geom_col(fill = "#54A24B", width = 0.75) +
labs(
title = "每週校外打工次數",
subtitle = "0 = 沒有打工;10+ = 每週 10 次或以上",
x = "每週打工次數",
y = "人數"
)
輸出裡最高的是 0:4881 人(87.2%)沒有打工。其餘次數的柱子都很矮,拿掉 0 之後最高的是 10+。
summarise()
一次算出人數、平均、標準差、中位數、四分位數、最小最大值。moments::skewness()
和 kurtosis() 是偏態、峰態。across()
把表上的數字四捨五入到小數兩位,再交給 kable()。
直方圖的 binwidth = 25 是每一柱寬 25
分。aes(y = after_stat(density)) 把縱軸改成密度,這樣才能和
geom_density()
的曲線畫在同一張圖上。兩條直線分別是平均數和中位數。盒形圖用
geom_boxplot(),看中間 50%
的範圍,以及特別高、特別低的點。
math_stats <- dat %>%
summarise(
人數 = sum(!is.na(pv1math)),
平均數 = mean(pv1math, na.rm = TRUE),
標準差 = sd(pv1math, na.rm = TRUE),
中位數 = median(pv1math, na.rm = TRUE),
Q1 = quantile(pv1math, 0.25, na.rm = TRUE),
Q3 = quantile(pv1math, 0.75, na.rm = TRUE),
最小值 = min(pv1math, na.rm = TRUE),
最大值 = max(pv1math, na.rm = TRUE),
偏態 = moments::skewness(pv1math, na.rm = TRUE),
峰態 = moments::kurtosis(pv1math, na.rm = TRUE)
)
kable(
math_stats %>% mutate(across(where(is.numeric), \(x) round(x, 2))),
caption = "數學分數的描述統計(常態的峰態大約是 3)"
)
| 人數 | 平均數 | 標準差 | 中位數 | Q1 | Q3 | 最小值 | 最大值 | 偏態 | 峰態 |
|---|---|---|---|---|---|---|---|---|---|
| 5599 | 537.82 | 109.73 | 541.62 | 460.38 | 618.76 | 219.06 | 917.38 | -0.1 | 2.58 |
ggplot(dat, aes(x = pv1math)) +
geom_histogram(
aes(y = after_stat(density)),
binwidth = 25,
fill = "#4C78A8",
color = "white",
boundary = 0
) +
geom_density(linewidth = 0.8, color = "#E45756") +
geom_vline(xintercept = math_stats$平均數, linetype = "dashed", color = "#E45756") +
geom_vline(xintercept = math_stats$中位數, linetype = "dotted", color = "#72B7B2") +
labs(
title = "數學成就分數分布",
subtitle = "紅虛線 = 平均數;青點線 = 中位數",
x = "PV1MATH",
y = "密度"
)
ggplot(dat, aes(x = "", y = pv1math)) +
geom_boxplot(fill = "#4C78A8", width = 0.35, outlier.alpha = 0.35) +
labs(title = "數學成就分數盒形圖", x = NULL, y = "PV1MATH")
表上的平均是 537.8、中位數是 541.6、標準差是 109.7,偏態 -0.1。直方圖是中間高、兩邊低,平均和中位數幾乎疊在一起;盒形圖上下都有離得比較遠的點。