Complete Code Listing

Below is the complete R code used in this chapter as a single script. You can copy this code into a Quarto document (.qmd file), render it, and reproduce all of the visualizations from this chapter.

Complete Code

# -------------------------------------------
# 1. Load packages and data
# -------------------------------------------
if(!require("pacman")) install.packages("pacman")
pacman::p_load("tidyverse", "psych", "knitr", "kableExtra", "gridExtra")

work <- read_delim(
  "https://ljkelly3141.github.io/datasets/bi-book/Absenteeism_at_work.csv",
  delim = ";"
)

# -------------------------------------------
# 2. Seasonality by month
# -------------------------------------------
absences_by_month <- work |>
  filter(`Month of absence` > 0) |>
  group_by(`Month of absence`) |>
  summarise(
    n_absences = n(),
    total_hours = sum(`Absenteeism time in hours`)
  ) |>
  mutate(
    Month = factor(month.abb[`Month of absence`], levels = month.abb)
  ) |>
  select(Month, n_absences, total_hours)

absences_by_month |>
  pivot_longer(cols = c(n_absences, total_hours),
               names_to = "metric", values_to = "value") |>
  ggplot(aes(x = Month, y = value, fill = metric)) +
  geom_col() +
  facet_wrap(~ metric, ncol = 1, scales = "free_y",
             labeller = as_labeller(c(
               n_absences = "Number of absences",
               total_hours = "Total hours absent"
             ))) +
  labs(x = NULL, y = NULL) +
  theme_minimal() +
  theme(legend.position = "none")

# -------------------------------------------
# 3. Day-of-week pattern
# -------------------------------------------
weekday_labels <- c("Mon", "Tue", "Wed", "Thu", "Fri")

absences_by_weekday <- work |>
  filter(`Month of absence` > 0) |>
  group_by(`Day of the week`) |>
  summarise(
    n_absences = n(),
    total_hours = sum(`Absenteeism time in hours`)
  ) |>
  mutate(
    Weekday = factor(weekday_labels[`Day of the week` - 1],
                     levels = weekday_labels)
  ) |>
  select(Weekday, n_absences, total_hours)

absences_by_weekday |>
  pivot_longer(cols = c(n_absences, total_hours),
               names_to = "metric", values_to = "value") |>
  ggplot(aes(x = Weekday, y = value, fill = metric)) +
  geom_col() +
  facet_wrap(~ metric, ncol = 1, scales = "free_y",
             labeller = as_labeller(c(
               n_absences = "Number of absences",
               total_hours = "Total hours absent"
             ))) +
  labs(x = NULL, y = NULL) +
  theme_minimal() +
  theme(legend.position = "none")

# -------------------------------------------
# 4. Distribution of absence duration
# -------------------------------------------
work |>
  filter(`Absenteeism time in hours` > 0) |>
  ggplot(aes(x = `Absenteeism time in hours`)) +
  geom_histogram(binwidth = 4, fill = "steelblue", color = "white") +
  labs(
    title = "Distribution of absence duration",
    x = "Hours absent (per event)",
    y = "Number of events"
  ) +
  theme_minimal()

# -------------------------------------------
# 5. Duration by reason group (box plot)
# -------------------------------------------
absence_with_reason <- work |>
  filter(`Absenteeism time in hours` > 0,
         `Reason for absence` > 0) |>
  mutate(
    reason_group = case_when(
      `Reason for absence` %in% 1:6   ~ "Serious illness",
      `Reason for absence` %in% 7:14  ~ "Other illness",
      `Reason for absence` %in% 15:17 ~ "Pregnancy / perinatal",
      `Reason for absence` %in% 18:21 ~ "Injury / external",
      `Reason for absence` %in% 22:28 ~ "Routine / administrative",
      TRUE                            ~ "Other"
    ),
    reason_group = factor(reason_group, levels = c(
      "Routine / administrative",
      "Other illness",
      "Serious illness",
      "Injury / external",
      "Pregnancy / perinatal"
    ))
  )

absence_with_reason |>
  ggplot(aes(x = reason_group, y = `Absenteeism time in hours`)) +
  geom_boxplot(fill = "steelblue", alpha = 0.6, outlier.color = "grey40") +
  scale_y_continuous(trans = "log10") +
  labs(
    title = "Absence duration by reason group",
    x = NULL,
    y = "Hours absent (log scale)"
  ) +
  theme_minimal()

# -------------------------------------------
# 6. Heat map: month x weekday
# -------------------------------------------
work |>
  filter(`Month of absence` > 0) |>
  mutate(
    Month   = factor(month.abb[`Month of absence`], levels = month.abb),
    Weekday = factor(c("Sun", "Mon", "Tue", "Wed",
                       "Thu", "Fri", "Sat")[`Day of the week`],
                     levels = c("Mon", "Tue", "Wed", "Thu", "Fri"))
  ) |>
  group_by(Month, Weekday) |>
  summarise(total_hours = sum(`Absenteeism time in hours`),
            .groups = "drop") |>
  ggplot(aes(x = Weekday, y = Month, fill = total_hours)) +
  geom_tile(color = "white") +
  scale_y_discrete(limits = rev(month.abb)) +
  scale_fill_viridis_c(option = "magma", direction = -1) +
  labs(
    title = "Total hours absent by month and weekday",
    x = NULL, y = NULL, fill = "Hours"
  ) +
  theme_minimal()

The transition from data preparation to modeling marks a shift from “what does the data look like?” to “what can the data tell us?”