載入套件

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 次或以上)"
)
每週打工次數(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)"
)
數學分數的描述統計(常態的峰態大約是 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。直方圖是中間高、兩邊低,平均和中位數幾乎疊在一起;盒形圖上下都有離得比較遠的點。