{"spec_id":"confusion-matrix","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' confusion-matrix: Confusion Matrix Heatmap\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 92/100 | Created: 2026-09-04\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens ------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nANNOT_DARK  <- \"#1A1A17\"\nANNOT_LIGHT <- \"#F0EFE8\"\n\n# --- Data: bird-species image classifier on a held-out test set --------\nclass_names <- c(\"Sparrow\", \"Finch\", \"Robin\", \"Cardinal\", \"Jay\")\nclass_sizes <- c(150, 130, 90, 70, 60)  # class imbalance in the test set\n\nconfusion_probs <- matrix(c(\n  0.88, 0.06, 0.02, 0.02, 0.02,\n  0.08, 0.82, 0.04, 0.03, 0.03,\n  0.03, 0.05, 0.85, 0.04, 0.03,\n  0.02, 0.03, 0.05, 0.87, 0.03,\n  0.03, 0.04, 0.03, 0.05, 0.85\n), nrow = length(class_names), byrow = TRUE)\n\ntrue_labels <- rep(class_names, times = class_sizes)\npredicted_labels <- unlist(lapply(seq_along(class_names), function(i) {\n  sample(class_names, size = class_sizes[i], replace = TRUE, prob = confusion_probs[i, ])\n}))\n\ncm <- as.data.frame(\n  table(\n    true_label      = factor(true_labels, levels = class_names),\n    predicted_label = factor(predicted_labels, levels = class_names)\n  )\n)\nnames(cm)[names(cm) == \"Freq\"] <- \"count\"\ncm <- cm %>%\n  mutate(\n    is_diagonal = true_label == predicted_label,\n    label_color = if_else(count > max(count) * 0.5, ANNOT_LIGHT, ANNOT_DARK)\n  )\n\n# --- Normalization margins: recall (row), precision (column), accuracy ----\ndiag_cells   <- filter(cm, is_diagonal)\nrow_totals   <- cm %>% group_by(true_label) %>% summarise(total = sum(count), .groups = \"drop\")\ncol_totals   <- cm %>% group_by(predicted_label) %>% summarise(total = sum(count), .groups = \"drop\")\noverall_acc  <- sum(diag_cells$count) / sum(cm$count)\n\nrecall_df <- diag_cells %>%\n  left_join(row_totals, by = \"true_label\") %>%\n  transmute(x = \"Recall\", y = as.character(true_label), label = sprintf(\"%.0f%%\", 100 * count / total))\n\nprecision_df <- diag_cells %>%\n  left_join(col_totals, by = \"predicted_label\") %>%\n  transmute(x = as.character(predicted_label), y = \"Precision\", label = sprintf(\"%.0f%%\", 100 * count / total))\n\ncorner_df <- data.frame(x = \"Recall\", y = \"Precision\", label = sprintf(\"%.0f%%\", 100 * overall_acc))\n\nmargin_df <- bind_rows(recall_df, precision_df, corner_df)\n\nx_levels <- c(class_names, \"Recall\")\ny_levels <- c(\"Precision\", rev(class_names))\n\n# --- Plot ----------------------------------------------------------------\np <- ggplot(cm, aes(x = predicted_label, y = true_label, fill = count)) +\n  geom_tile(color = PAGE_BG, linewidth = 1.5) +\n  geom_tile(\n    data = filter(cm, is_diagonal),\n    fill = NA, color = INK, linewidth = 1.2\n  ) +\n  geom_text(aes(label = count, color = label_color), size = 4.2, fontface = \"bold\") +\n  geom_tile(\n    data = margin_df, aes(x = x, y = y),\n    inherit.aes = FALSE, fill = ELEVATED_BG, color = PAGE_BG, linewidth = 1.5\n  ) +\n  geom_text(\n    data = margin_df, aes(x = x, y = y, label = label),\n    inherit.aes = FALSE, color = INK, size = 4.0, fontface = \"italic\"\n  ) +\n  scale_color_identity() +\n  scale_fill_gradient(low = \"#009E73\", high = \"#4467A3\", name = \"Count\") +\n  scale_x_discrete(limits = x_levels, expand = c(0, 0)) +\n  scale_y_discrete(limits = y_levels, expand = c(0, 0)) +\n  coord_fixed() +\n  labs(\n    x     = \"Predicted Label\",\n    y     = \"True Label\",\n    title = \"confusion-matrix · r · ggplot2 · anyplot.ai\"\n  ) +\n  theme_minimal(base_size = 8) +\n  theme(\n    plot.background    = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background   = element_rect(fill = PAGE_BG, color = NA),\n    panel.grid         = element_blank(),\n    axis.title         = element_text(color = INK, size = 10),\n    axis.text          = element_text(color = INK_SOFT, size = 8),\n    axis.ticks         = element_blank(),\n    plot.title         = element_text(color = INK, size = 12),\n    legend.background  = element_rect(fill = PAGE_BG, color = NA),\n    legend.text        = element_text(color = INK_SOFT, size = 8),\n    legend.title       = element_text(color = INK, size = 10),\n    legend.key.height  = unit(0.9, \"cm\"),\n    legend.key.width   = unit(0.4, \"cm\")\n  )\n\n# --- Save ------------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 6,\n  height   = 6,\n  units    = \"in\",\n  dpi      = 400\n)\n"}