Files
party2d/validation/run_postprocessing.R
T
2026-08-13 21:00:17 +00:00

465 lines
24 KiB
R
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env Rscript
options(stringsAsFactors = FALSE)
required_packages <- c("ggplot2", "sandwich", "lme4")
missing_packages <- required_packages[!vapply(required_packages, requireNamespace, logical(1), quietly = TRUE)]
if (length(missing_packages)) stop("Missing R packages: ", paste(missing_packages, collapse = ", "))
library(ggplot2)
script_args <- commandArgs(trailingOnly = FALSE)
file_arg <- script_args[grepl("^--file=", script_args)][1]
script_path <- normalizePath(sub("^--file=", "", file_arg))
repo_root <- normalizePath(file.path(dirname(script_path), ".."))
setwd(repo_root)
output_dir <- file.path("revision", "outputs")
figure_dir <- file.path("revision", "figures")
dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
dir.create(figure_dir, recursive = TRUE, showWarnings = FALSE)
panel_file <- file.path("data", "releases", "party_2d_election_year_panel_v0.csv.gz")
annual_file <- file.path("data", "releases", "party_2d_annual_model_output_v0.csv.gz")
no_vparty_file <- "/projects/party4d_validation_runs/no_vparty/outputs/estimations/latest/party_positions_2026-06-05_14-57-17.csv"
for (f in c(panel_file, annual_file, no_vparty_file)) {
if (!file.exists(f)) stop("Required input not found: ", f)
}
panel <- read.csv(panel_file, check.names = FALSE)
annual <- read.csv(annual_file, check.names = FALSE)
no_vparty <- read.csv(no_vparty_file, check.names = FALSE)
families <- read.csv(file.path("data", "party_families.csv"), check.names = FALSE)
names(families)[names(families) == "partyfacts_id"] <- "party_id"
iso_names <- c(
AL="Albania", AM="Armenia", AR="Argentina", AT="Austria", AU="Australia",
AZ="Azerbaijan", BA="Bosnia and Herzegovina", BE="Belgium", BG="Bulgaria",
BO="Bolivia", BR="Brazil", BY="Belarus", CA="Canada", CH="Switzerland",
CL="Chile", CO="Colombia", CR="Costa Rica", CY="Cyprus", CZ="Czechia",
DE="Germany", DK="Denmark", DO="Dominican Republic", EC="Ecuador",
EE="Estonia", ES="Spain", FI="Finland", FR="France", GB="United Kingdom",
GE="Georgia", GR="Greece", HR="Croatia", HU="Hungary", IE="Ireland",
IL="Israel", IS="Iceland", IT="Italy", JP="Japan", KR="South Korea",
LK="Sri Lanka", LT="Lithuania", LU="Luxembourg", LV="Latvia", MD="Moldova",
ME="Montenegro", MK="North Macedonia", MT="Malta", MX="Mexico",
NL="Netherlands", NO="Norway", NZ="New Zealand", PA="Panama", PE="Peru",
PL="Poland", PT="Portugal", RO="Romania", RS="Serbia", RU="Russia",
SE="Sweden", SI="Slovenia", SK="Slovakia", TR="Türkiye", UA="Ukraine",
US="United States", UY="Uruguay", ZA="South Africa"
)
region_for <- function(country) {
europe <- c("AL","AT","BA","BE","BG","BY","CH","CY","CZ","DE","DK","EE","ES","FI","FR","GB","GE","GR","HR","HU","IE","IS","IT","LT","LU","LV","MD","ME","MK","MT","NL","NO","PL","PT","RO","RS","RU","SE","SI","SK","TR","UA")
latin <- c("AR","BO","BR","CL","CO","CR","DO","EC","MX","PA","PE","UY")
north_america <- c("CA", "US")
asia_pacific <- c("AU", "JP", "KR", "LK", "NZ")
out <- rep("Other", length(country))
out[country %in% europe] <- "Europe"
out[country %in% latin] <- "Latin America"
out[country %in% north_america] <- "North America"
out[country %in% asia_pacific] <- "Asia-Pacific"
out
}
family_names <- c(
com="Communist/Far Left", eco="Green/Ecological", soc="Social Democratic",
chr="Christian Democratic", agr="Agrarian", lib="Liberal", con="Conservative",
right="Radical Right", spec="Special issue", other="Other", nofam="Unclassified"
)
theme_revision <- function() {
theme_minimal(base_size = 9) +
theme(
panel.grid.minor = element_blank(),
strip.text = element_text(face = "bold"),
legend.position = "bottom",
plot.title.position = "plot"
)
}
write_clean_csv <- function(x, path) {
write.csv(x, path, row.names = FALSE, na = "")
message("Wrote ", path, " (", nrow(x), " rows)")
}
# ---------------------------------------------------------------------------
# Versioned country coverage
# ---------------------------------------------------------------------------
country_rows <- lapply(sort(unique(panel$country)), function(cc) {
d <- panel[panel$country == cc, ]
tab <- table(d$source_support_class)
get_n <- function(label) if (label %in% names(tab)) unname(tab[label]) else 0
data.frame(
release = "v0", iso2 = cc, country = unname(iso_names[cc]),
first_year = min(d$year), last_year = max(d$year),
parties = length(unique(d$party_id)), election_year_rows = nrow(d),
both_text_expert = get_n("both_direct_or_nearby"),
text_only = get_n("text_only_direct_or_nearby"),
expert_only = get_n("expert_only_direct_or_nearby"),
temporal_propagation = get_n("temporal_propagation")
)
})
country_coverage <- do.call(rbind, country_rows)
write_clean_csv(country_coverage, file.path("metadata", "country_coverage_v0.csv"))
# ---------------------------------------------------------------------------
# Source-support balance with uncertainty and hierarchical heterogeneity
# ---------------------------------------------------------------------------
balance <- panel[is.na(panel$pervote) | panel$pervote > 0, ]
balance$pervote[is.na(balance$pervote)] <- 0
balance$decade <- factor(floor(balance$year / 10) * 10)
balance$region <- factor(region_for(balance$country))
balance$country <- factor(balance$country)
balance$text_only <- as.integer(balance$source_support_class == "text_only_direct_or_nearby")
balance$expert_only <- as.integer(balance$source_support_class == "expert_only_direct_or_nearby")
balance$temporal <- as.integer(balance$source_support_class == "temporal_propagation")
class_labels <- c(
text_only="Text only", expert_only="Expert only", temporal="Temporal propagation"
)
pooled_rows <- list()
interaction_rows <- list()
row_i <- 1
interaction_i <- 1
for (dimension in c("economic_lr", "galtan")) {
form <- as.formula(paste(dimension, "~ text_only + expert_only + temporal + factor(country) + decade + log1p(pervote)"))
model <- lm(form, data = balance)
cluster_vcov <- sandwich::vcovCL(model, cluster = balance$country, type = "HC1")
for (term in names(class_labels)) {
estimate <- unname(coef(model)[term])
se <- sqrt(cluster_vcov[term, term])
category <- switch(term,
text_only="text_only_direct_or_nearby",
expert_only="expert_only_direct_or_nearby",
temporal="temporal_propagation")
pooled_rows[[row_i]] <- data.frame(
dimension = dimension, comparison = class_labels[term],
category_n = sum(balance$source_support_class == category),
model_n = nobs(model), estimate = estimate, clustered_se = se,
ci_lower = estimate - 1.96 * se, ci_upper = estimate + 1.96 * se,
p_value = 2 * pnorm(abs(estimate / se), lower.tail = FALSE)
)
row_i <- row_i + 1
}
# Partially pool the well-populated text-only contrast across region and
# decade. Expert-only and temporal-propagation contrasts remain pooled
# because their samples are too sparse for stable varying slopes.
varying_form <- as.formula(paste(
dimension,
"~ expert_only + temporal + text_only + log1p(pervote) +",
"(1 | country) + (0 + text_only | region) + (0 + text_only | decade)"
))
varying_model <- lme4::lmer(varying_form, data = balance, REML = TRUE,
control = lme4::lmerControl(check.nobs.vs.nRE = "ignore"))
fixed_text <- lme4::fixef(varying_model)["text_only"]
fixed_var <- as.matrix(vcov(varying_model))["text_only", "text_only"]
re <- lme4::ranef(varying_model, condVar = TRUE)
for (group_type in c("region", "decade")) {
group_values <- rownames(re[[group_type]])
post_var <- attr(re[[group_type]], "postVar")
for (group_index in seq_along(group_values)) {
g <- group_values[group_index]
template <- balance[as.character(balance[[group_type]]) == g, , drop = FALSE]
estimate <- fixed_text + re[[group_type]][g, "text_only"]
# Approximate conditional interval. Adding fixed-effect and conditional
# random-effect variances is conservative because their covariance is
# not exposed by ranef().
se <- sqrt(fixed_var + post_var[1, 1, group_index])
interaction_rows[[interaction_i]] <- data.frame(
dimension = dimension, group_type = group_type, group = as.character(g),
comparison = "Text only", group_n = nrow(template),
text_only_n = sum(template$source_support_class == "text_only_direct_or_nearby"),
estimate = unname(estimate), approximate_se = unname(se),
ci_lower = unname(estimate - 1.96 * se), ci_upper = unname(estimate + 1.96 * se),
model_n = nobs(varying_model), singular_fit = lme4::isSingular(varying_model)
)
interaction_i <- interaction_i + 1
}
}
}
pooled <- do.call(rbind, pooled_rows)
interactions <- do.call(rbind, interaction_rows)
write_clean_csv(pooled, file.path(output_dir, "source_support_pooled.csv"))
write_clean_csv(interactions, file.path(output_dir, "source_support_interactions.csv"))
interactions$dimension_label <- ifelse(interactions$dimension == "economic_lr", "Economic", "Cultural")
interactions$group_type_label <- ifelse(interactions$group_type == "region", "Region", "Decade")
p_balance <- ggplot(interactions, aes(x = estimate, y = reorder(group, estimate))) +
geom_vline(xintercept = 0, colour = "grey40", linewidth = 0.4, linetype = 2) +
geom_errorbar(aes(xmin = ci_lower, xmax = ci_upper), orientation = "y", width = 0, linewidth = 0.4, colour = "grey20") +
geom_point(size = 1.6, colour = "black") +
facet_wrap(~dimension_label + group_type_label, scales = "free_y", ncol = 2) +
labs(x = "Adjusted difference from overlapping text-and-expert support", y = NULL) +
theme_revision()
ggsave(file.path(figure_dir, "source_support_heterogeneity.pdf"), p_balance,
width = 8.2, height = 6.8, device = cairo_pdf)
# ---------------------------------------------------------------------------
# V-Party ablation: case and subgroup sensitivity
# ---------------------------------------------------------------------------
keys <- c("party_id", "country", "year")
vparty <- merge(panel, no_vparty, by = keys, suffixes = c("_production", "_no_vparty"))
vparty <- merge(vparty, families, by = "party_id", all.x = TRUE)
vparty$family[is.na(vparty$family)] <- "nofam"
vparty$family_label <- unname(family_names[vparty$family])
vparty$region <- region_for(vparty$country)
vparty$decade <- floor(vparty$year / 10) * 10
for (dimension in c("economic_lr", "galtan")) {
prod <- vparty[[paste0(dimension, "_production")]]
ablated <- vparty[[paste0(dimension, "_no_vparty")]]
vparty[[paste0(dimension, "_difference")]] <- ablated - prod
vparty[[paste0(dimension, "_abs_difference")]] <- abs(ablated - prod)
width_prod <- vparty[[paste0(dimension, "_q975_production")]] - vparty[[paste0(dimension, "_q025_production")]]
width_ablation <- vparty[[paste0(dimension, "_q975_no_vparty")]] - vparty[[paste0(dimension, "_q025_no_vparty")]]
vparty[[paste0(dimension, "_interval_width_production")]] <- width_prod
vparty[[paste0(dimension, "_interval_width_no_vparty")]] <- width_ablation
vparty[[paste0(dimension, "_interval_width_change")]] <- width_ablation - width_prod
}
summary_by <- function(data, group_var, dimension) {
split_data <- split(data, data[[group_var]], drop = TRUE)
rows <- lapply(names(split_data), function(g) {
d <- split_data[[g]]
absdiff <- d[[paste0(dimension, "_abs_difference")]]
diff <- d[[paste0(dimension, "_difference")]]
wprod <- d[[paste0(dimension, "_interval_width_production")]]
wabl <- d[[paste0(dimension, "_interval_width_no_vparty")]]
data.frame(
dimension = dimension, group_type = group_var, group = g,
n = nrow(d), parties = length(unique(d$party_id)),
mean_abs_difference = mean(absdiff), median_abs_difference = median(absdiff),
p90_abs_difference = unname(quantile(absdiff, .90)),
p95_abs_difference = unname(quantile(absdiff, .95)),
mean_signed_difference = mean(diff),
mean_interval_width_production = mean(wprod),
mean_interval_width_no_vparty = mean(wabl),
mean_interval_width_change = mean(wabl - wprod)
)
})
do.call(rbind, rows)
}
vparty_summaries <- do.call(rbind, lapply(c("country", "decade", "region", "family_label", "source_support_class"), function(g) {
do.call(rbind, lapply(c("economic_lr", "galtan"), function(d) summary_by(vparty, g, d)))
}))
write_clean_csv(vparty, file.path(output_dir, "vparty_sensitivity_matched_rows.csv"))
write_clean_csv(vparty_summaries, file.path(output_dir, "vparty_sensitivity_groups.csv"))
top_cases <- do.call(rbind, lapply(c("economic_lr", "galtan"), function(dimension) {
ord <- order(vparty[[paste0(dimension, "_abs_difference")]], decreasing = TRUE)
d <- vparty[head(ord, 30), ]
data.frame(
dimension = dimension, party_id = d$party_id,
party_name = d$party_name_english, party_short = d$party_name_short,
country = d$country, year = d$year,
production = d[[paste0(dimension, "_production")]],
no_vparty = d[[paste0(dimension, "_no_vparty")]],
signed_difference = d[[paste0(dimension, "_difference")]],
absolute_difference = d[[paste0(dimension, "_abs_difference")]],
production_interval_width = d[[paste0(dimension, "_interval_width_production")]],
no_vparty_interval_width = d[[paste0(dimension, "_interval_width_no_vparty")]],
source_support_class = d$source_support_class,
family = d$family_label
)
}))
write_clean_csv(top_cases, file.path(output_dir, "vparty_sensitivity_top_cases.csv"))
country_plot <- vparty_summaries[vparty_summaries$group_type == "country" & vparty_summaries$n >= 10, ]
country_plot <- do.call(rbind, lapply(split(country_plot, country_plot$dimension), function(d) {
head(d[order(d$mean_abs_difference, decreasing = TRUE), ], 15)
}))
country_plot$dimension_label <- ifelse(country_plot$dimension == "economic_lr", "Economic", "Cultural")
p_vparty <- ggplot(country_plot, aes(x = mean_abs_difference, y = reorder(group, mean_abs_difference))) +
geom_point(aes(size = n), colour = "black") +
facet_wrap(~dimension_label, scales = "free_y") +
labs(x = "Mean absolute change after removing V-Party", y = "Country", size = "Party-years") +
theme_revision()
ggsave(file.path(figure_dir, "vparty_sensitivity_countries.pdf"), p_vparty,
width = 7.2, height = 5.8, device = cairo_pdf)
# ---------------------------------------------------------------------------
# Illustrative trajectories and landmark cases
# ---------------------------------------------------------------------------
trajectory_ids <- c(383, 1567, 379, 409)
traj <- panel[panel$party_id %in% trajectory_ids, ]
traj$party_label <- factor(traj$party_id, levels = trajectory_ids,
labels = c("Germany: Social Democrats", "United Kingdom: Conservatives", "Denmark: Social Democrats", "Sweden: Sweden Democrats"))
traj_long <- rbind(
data.frame(traj[c("party_id","party_label","country","year","source_support_class")],
dimension="Economic", estimate=traj$economic_lr,
lower=traj$economic_lr_q025, upper=traj$economic_lr_q975),
data.frame(traj[c("party_id","party_label","country","year","source_support_class")],
dimension="Cultural", estimate=traj$galtan,
lower=traj$galtan_q025, upper=traj$galtan_q975)
)
write_clean_csv(traj_long, file.path(output_dir, "party_trajectory_plot_data.csv"))
p_traj <- ggplot(traj_long, aes(x = year, y = estimate)) +
geom_ribbon(aes(ymin = lower, ymax = upper), fill = "grey85", alpha = 0.5) +
geom_line(colour = "black", linewidth = .65) +
geom_point(colour = "black", size = .9) +
facet_grid(dimension ~ party_label) +
scale_y_continuous(limits = c(0, 1), breaks = c(0, .5, 1)) +
labs(x = "Election year", y = "Posterior position (0–1)") +
theme_revision() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
ggsave(file.path(figure_dir, "party_trajectories.pdf"), p_traj,
width = 10.2, height = 4.9, device = cairo_pdf)
landmark_spec <- data.frame(
party_id = c(383,383,1375,1375,1516,1516,1567,1567,487,487,409,409,433,433,1545,432,809),
target_year = c(1972,2021,1983,2021,1983,1997,1979,2019,1994,2022,2010,2022,1988,2022,2021,2020,2020),
label = c("German Social Democrats 1972","German Social Democrats 2021","German Christian Democrats 1983","German Christian Democrats 2021","Labour 1983","Labour 1997",
"Conservatives 1979","Conservatives 2019","Swedish Social Democrats 1994","Swedish Social Democrats 2022",
"Sweden Democrats 2010","Sweden Democrats 2022","French National Front 1988","French National Front 2022",
"The Left 2021","US Democrats 2020","US Republicans 2020")
)
landmark_rows <- lapply(seq_len(nrow(landmark_spec)), function(i) {
spec <- landmark_spec[i, ]
d <- panel[panel$party_id == spec$party_id & abs(panel$year - spec$target_year) <= 3, ]
if (!nrow(d)) return(data.frame(spec, status="missing", actual_year=NA, economic_lr=NA, galtan=NA))
d <- d[which.min(abs(d$year - spec$target_year)), ]
data.frame(spec, status=ifelse(d$year == spec$target_year, "exact", "nearest within 3 years"),
actual_year=d$year, party_name=d$party_name_english,
country=d$country, economic_lr=d$economic_lr, economic_lower=d$economic_lr_q025,
economic_upper=d$economic_lr_q975, galtan=d$galtan,
galtan_lower=d$galtan_q025, galtan_upper=d$galtan_q975,
source_support_class=d$source_support_class)
})
landmarks <- do.call(rbind, landmark_rows)
landmarks$era <- ifelse(landmarks$target_year < 2000, "Historical & Cold War Era (1970–1999)", "Contemporary Era (2000–2022)")
landmarks$era <- factor(landmarks$era, levels = c("Historical & Cold War Era (1970–1999)", "Contemporary Era (2000–2022)"))
write_clean_csv(landmarks, file.path(output_dir, "party_landmark_plot_data.csv"))
plot_landmarks <- landmarks[!is.na(landmarks$economic_lr), ]
label_offsets <- data.frame(
label = landmark_spec$label,
dx = c(-.060,-.060,-.055,.045,-.040,-.060,.035,-.055,-.005,.080,-.060,.060,.030,-.030,.035,.080,.025),
dy = c(.070,.040,.055,-.035,-.070,-.040,-.025,.030,.040,.060,-.035,.035,.035,.050,.035,-.060,.035)
)
plot_landmarks <- merge(plot_landmarks, label_offsets, by = "label", all.x = TRUE, sort = FALSE)
plot_landmarks$label_x <- pmin(.98, pmax(.02, plot_landmarks$economic_lr + plot_landmarks$dx))
plot_landmarks$label_y <- pmin(.98, pmax(.02, plot_landmarks$galtan + plot_landmarks$dy))
# Country shapes for clear grayscale distinction
country_shapes <- c(DE = 16, GB = 17, SE = 15, FR = 18, US = 8)
p_land <- ggplot(plot_landmarks, aes(x = economic_lr, y = galtan)) +
geom_errorbar(aes(ymin = galtan_lower, ymax = galtan_upper), width = 0, alpha = .35, colour = "grey30") +
geom_errorbar(aes(xmin = economic_lower, xmax = economic_upper), orientation = "y", width = 0, alpha = .35, colour = "grey30") +
geom_segment(aes(xend = label_x, yend = label_y), linewidth = .2, colour = "grey50") +
geom_point(aes(shape = country), size = 2.2, fill = "black", colour = "black") +
geom_text(aes(x = label_x, y = label_y, label = label), size = 2.4,
check_overlap = TRUE, show.legend = FALSE) +
facet_wrap(~era, ncol = 2) +
scale_shape_manual(values = country_shapes) +
scale_x_continuous(limits = c(0,1), breaks = c(0,.25,.5,.75,1)) +
scale_y_continuous(limits = c(0,1), breaks = c(0,.25,.5,.75,1)) +
labs(x = "Economic: left (0) to right (1)",
y = "Cultural: cosmopolitan (0) to traditionalist (1)", shape = "Country") +
theme_revision()
ggsave(file.path(figure_dir, "party_landmarks.pdf"), p_land,
width = 9.2, height = 5.2, device = cairo_pdf)
# ---------------------------------------------------------------------------
# Predictive-interval calibration and concentration of misses
# ---------------------------------------------------------------------------
ppc_dir <- file.path(output_dir, "ppc")
ppc_item_file <- file.path(ppc_dir, "posterior_predictive_by_dimension_item.csv")
ppc_calibration_file <- file.path(ppc_dir, "posterior_predictive_calibration_curve.csv")
if (file.exists(ppc_item_file) && file.exists(ppc_calibration_file)) {
ppc_items <- read.csv(ppc_item_file, check.names = FALSE)
ppc_items$dimension <- ifelse(ppc_items$dim_idx == 1, "Economic", "Cultural")
ppc_items$item_label <- c(
gender_vparty="V-Party gender-policy item", relig_vparty="V-Party religion-policy item",
welf_vparty="V-Party welfare-position item", culsup_vparty="V-Party cultural-superiority item",
immig_vparty="V-Party immigration-policy item", lgbt_vparty="V-Party LGBT-policy item",
lrecon_vparty="V-Party economic LR", galtan_ches="CHES GAL--TAN",
lrecon_ches="CHES economic LR", libcon_gps="GPS cultural",
lrecon_gps="GPS economic LR", lrecon_poppa="POPPA economic LR"
)[ppc_items$var]
ppc_items$item_label[is.na(ppc_items$item_label)] <- ppc_items$var[is.na(ppc_items$item_label)]
ppc_items$item_label <- factor(ppc_items$item_label,
levels = rev(ppc_items$item_label[order(ppc_items$observed_coverage)]))
p_ppc <- ggplot(ppc_items, aes(x = observed_coverage, y = item_label)) +
geom_vline(xintercept = .95, linetype = 2, colour = "grey40", linewidth = .35) +
geom_errorbar(aes(xmin = coverage_ci_lower, xmax = coverage_ci_upper),
orientation = "y", width = 0, linewidth = .35, colour = "grey20") +
geom_point(aes(size = n, shape = project), colour = "black", fill = "gray30") +
facet_wrap(~dimension, scales = "free_y") +
scale_x_continuous(limits = c(.70, 1), breaks = c(.70,.80,.90,.95,1),
labels = function(x) paste0(round(100*x), "%")) +
labs(x = "Observed coverage of nominal 95% predictive interval", y = NULL,
shape = "Source project", size = "Ratings") +
theme_revision()
ggsave(file.path(figure_dir, "predictive_coverage_by_item.pdf"), p_ppc,
width = 7.8, height = 5.4, device = cairo_pdf)
ppc_cal <- read.csv(ppc_calibration_file, check.names = FALSE)
ppc_cal$dimension <- ifelse(ppc_cal$dim_idx == 1, "Economic", "Cultural")
p_cal <- ggplot(ppc_cal, aes(x = nominal_level, y = observed_coverage, linetype = dimension, shape = dimension)) +
geom_abline(slope = 1, intercept = 0, linetype = 3, colour = "grey50") +
geom_line(linewidth = .6, colour = "black") + geom_point(size = 1.8, colour = "black") +
scale_x_continuous(limits = c(.45, 1), breaks = c(.5,.8,.9,.95,1),
labels = function(x) paste0(round(100*x), "%")) +
scale_y_continuous(limits = c(.4, 1), breaks = c(.4,.5,.6,.7,.8,.9,1),
labels = function(x) paste0(round(100*x), "%")) +
labs(x = "Nominal interval level", y = "Observed coverage", linetype = "Dimension", shape = "Dimension") +
coord_equal() + theme_revision()
ggsave(file.path(figure_dir, "predictive_calibration_curve.pdf"), p_cal,
width = 5.7, height = 5.0, device = cairo_pdf)
ppc_obs_file <- file.path(ppc_dir, "posterior_predictive_observations.csv")
if (file.exists(ppc_obs_file)) {
ppc_obs <- read.csv(ppc_obs_file, check.names = FALSE)
ppc_obs$covered_95 <- tolower(as.character(ppc_obs$covered_95)) == "true"
expert_input <- read.csv(file.path("data", "expert.csv"), check.names = FALSE)
key <- function(d) paste(d$party, d$country, d$year, d$var, sprintf("%.10f", d$val), sep = "|")
ppc_obs$n_experts <- expert_input$n_experts[match(key(ppc_obs), key(expert_input))]
ppc_obs$interval_width <- ppc_obs$pred_upper - ppc_obs$pred_lower
ppc_obs$fitted_bin <- cut(ppc_obs$pred_median,
breaks = c(-Inf,.10,.25,.50,.75,.90,Inf), right = FALSE,
labels = c("<0.10","0.10--0.25","0.25--0.50","0.50--0.75","0.75--0.90",">=0.90"))
ppc_obs$expert_count_bin <- cut(ppc_obs$n_experts,
breaks = c(-Inf,1,3,5,10,Inf), right = TRUE,
labels = c("1","2--3","4--5","6--10",">10"))
width_breaks <- unique(quantile(ppc_obs$interval_width, seq(0, 1, .25), na.rm = TRUE))
ppc_obs$interval_width_quartile <- cut(ppc_obs$interval_width, breaks = width_breaks,
include.lowest = TRUE, labels = paste0("Q", seq_len(length(width_breaks)-1)))
summarize_ppc_bins <- function(variable) {
pieces <- split(ppc_obs, interaction(ppc_obs$dim_idx, ppc_obs[[variable]], drop = TRUE))
do.call(rbind, lapply(pieces, function(d) {
data.frame(
dim_idx = d$dim_idx[1], group_type = variable,
group = as.character(d[[variable]][1]), n = nrow(d),
covered = sum(d$covered_95), observed_coverage = mean(d$covered_95),
mean_interval_width = mean(d$interval_width),
mean_fitted_value = mean(d$pred_median), mean_expert_count = mean(d$n_experts, na.rm = TRUE)
)
}))
}
ppc_residual <- do.call(rbind, lapply(
c("fitted_bin", "expert_count_bin", "interval_width_quartile"), summarize_ppc_bins))
write_clean_csv(ppc_residual, file.path(ppc_dir, "posterior_predictive_residual_patterns.csv"))
}
}
message("Post-processing complete.")