## ----kanon-worked-examples----------------------------------------------------
library(dataganger)
library(knitr)

# Everything below uses only exported DataGangeR functions, so you can run it
# yourself. A "roles" frame just needs `variable` and `disclosure_role`
# ("quasi" marks a quasi-identifier, "direct" is removed, "none" is left alone).
qi_roles <- function(data, qi_cols, drop_cols = "id") {
  data.frame(
    variable = names(data),
    disclosure_role = ifelse(
      names(data) %in% qi_cols, "quasi",
      ifelse(names(data) %in% drop_cols, "direct", "none")
    ),
    stringsAsFactors = FALSE
  )
}

# Probe one configuration: returns the kanon outcome (infeasible? how many
# quasi-identifier cells had to be suppressed?) without printing warnings.
probe_kanon <- function(data, qi_cols, k) {
  out <- withCallingHandlers(
    enforce_kanon(data, qi_roles(data, qi_cols), k = k),
    warning = function(w) invokeRestart("muffleWarning")
  )
  attr(out, "kanon", exact = TRUE)
}

individual_qi <- c("age", "sex", "education", "smoker")

# Escape route 1: walk k down from 5 to the largest value that works
# (never below 3 - smaller values give close to no protection).
feasible_k <- NULL
for (k_try in 5:3) {
  info <- probe_kanon(individual_sample, individual_qi, k_try)
  if (!isTRUE(info$infeasible)) {
    feasible_k <- k_try
    feasible_k_info <- info
    break
  }
}

# Escape route 2: generate more synthetic rows, so each quasi-identifier
# combination is shared by more rows. The engine is pinned so the numbers
# do not depend on which synthesis packages are installed.
more_rows_spec <- synth_spec(
  purpose = "development", n = 1000, seed = 1, engine = "internal"
)
more_rows <- withCallingHandlers(
  synthesize_data(individual_sample, more_rows_spec,
                  roles = qi_roles(individual_sample, individual_qi)),
  warning = function(w) invokeRestart("muffleWarning")
)
more_rows_info <- attr(more_rows, "kanon", exact = TRUE)

# Escape route 3: shrink the quasi-identifier set (age drives the sparsity:
# it has the most distinct values of the four).
drop_age_info <- probe_kanon(
  individual_sample, c("sex", "education", "smoker"), 5
)

suppressed_text <- function(info) {
  n <- info$suppressed_cells
  sprintf("%d suppressed QI cells", if (is.null(n)) 0L else n)
}

individual_table <- data.frame(
  option = c(
    "Keep age + sex + education + smoker at k = 5",
    sprintf("Apply k = %d", feasible_k),
    "Generate 1000 rows at k = 5",
    "Mark age as not part of the QI set"
  ),
  feasible_at_k = c(
    "No",
    sprintf("Yes (k = %d)", feasible_k),
    if (isTRUE(more_rows_info$infeasible)) "No" else "Yes (k = 5)",
    if (isTRUE(drop_age_info$infeasible)) "No" else "Yes (k = 5)"
  ),
  cost = c(
    "No k-anonymity protection applied",
    suppressed_text(feasible_k_info),
    suppressed_text(more_rows_info),
    suppressed_text(drop_age_info)
  ),
  stringsAsFactors = FALSE
)

# The temporal and geographic samples are aggregate-shaped (site-day records
# and region summaries, not people), so no column is marked as identifying
# in combination and the k-anonymity step never engages.
temporal_table <- data.frame(
  option = "Keep the combination question at none for all columns",
  feasible_at_k = "Not applicable",
  cost = sprintf(
    "No k-anonymity step; %d QI columns selected because rows are site-day records, not people",
    sum(qi_roles(temporal_sample, character(0))$disclosure_role == "quasi")
  ),
  stringsAsFactors = FALSE
)

geographic_table <- data.frame(
  option = "Keep the combination question at none for all columns",
  feasible_at_k = "Not applicable",
  cost = sprintf(
    "No k-anonymity step; %d QI columns selected because rows are region summaries, not people",
    sum(qi_roles(geographic_sample, character(0))$disclosure_role == "quasi")
  ),
  stringsAsFactors = FALSE
)

## -----------------------------------------------------------------------------
kable(individual_table, col.names = c("Option", "Feasible at k", "Cost"))

## -----------------------------------------------------------------------------
kable(temporal_table, col.names = c("Option", "Feasible at k", "Cost"))

## -----------------------------------------------------------------------------
kable(geographic_table, col.names = c("Option", "Feasible at k", "Cost"))

