Files
party2d/models/stan_model_2dim_v6.stan
2026-06-15 11:33:18 +02:00

596 lines
24 KiB
Plaintext

// =============================================================================
// 2D BIPOLAR LATENT TRAIT MODEL V6 - Hierarchical L-R Weights
// =============================================================================
//
// V6 CHANGE: Hierarchical L-R weights varying by country, source, and decade
// - Replaces global simplex[2] lr_weights with additive logit-scale model
// - logit_weight[n] = global + country_offset[c] + source_offset[k] + decade_offset[d]
// - All offsets use non-centered parameterization with estimated sigma hyperparameters
// - Data-poor contexts shrink toward global mean (~55 new scalar parameters)
//
// PRESERVED FROM V5:
// - Beta-Binomial expert likelihood with K-scaling
// - V-Party cultural expansion (5 native items + v2pawelf)
// - Mean-constituent model (individual party estimates for union members)
// - Segment-based indexing (S segments, R segment-years)
// - 3-level hierarchical variance (global -> country -> family)
// - Random walk dynamics within segments
// - CDU anchor for scale identification (CDU=1375, not CDU/CSU=211)
// - Non-centered parameterization for efficiency
// - Zero-inflation model for manifesto data
// - Country-item intercepts (coding convention differences across countries)
// - Binomial-logit likelihood for text data (unchanged)
//
// =============================================================================
data {
// Segment structure
int<lower=1> S; // Number of segments
int<lower=1> P; // Number of countries
int<lower=1> R; // Total unique segment-year combinations
int<lower=1> T_year; // Total number of years
array[S] int<lower=1> len_theta_ts; // Number of years per segment
// Country membership for each segment
array[S] int<lower=1, upper=P> segment_country;
// Segment family data
int<lower=1> F; // Number of party families
array[S] int<lower=1, upper=F> segment_family; // Family index for each segment
// =========================================================================
// Text data (manifesto + PolDem)
// =========================================================================
int<lower=1> N_man; // Number of text observations
int<lower=1> K_man; // Number of unique text items
array[N_man] int<lower=1, upper=K_man> kk_man; // Text item index
array[N_man] int<lower=1, upper=S> ss_man; // Segment index (first constituent for unions)
array[N_man] int<lower=1, upper=P> pp_man; // Country index
array[N_man] int<lower=0> positive; // Positive mentions
array[N_man] int<lower=0> sample; // Total sample size
array[N_man] int<lower=1, upper=T_year> year_for_man; // Year index
// Dimension and direction for text data
array[N_man] int<lower=1, upper=2> dim_idx_man; // 1=economic, 2=galtan
array[N_man] int<lower=-1, upper=1> direction_man; // +1=right/TAN, -1=left/GAL
// V4: Constituent structure for manifesto observations
int<lower=1> N_const_man_total; // Total entries in const_rr_man
array[N_man] int<lower=1> n_const_man; // Number of constituents per obs
array[N_man] int<lower=1> const_offset_man; // Offset into const_rr_man
array[N_const_man_total] int<lower=1, upper=R> const_rr_man; // Constituent rr indices
// Country-item-year data (used by zero-inflation model)
int<lower=1> N_ciy; // Number of unique country-item-year combinations
array[N_man] int<lower=1, upper=N_ciy> ciy_idx;
// =========================================================================
// Expert dimension-specific data (V5: integer observations + scale size)
// =========================================================================
int<lower=1> N_exp_dim; // Number of dimension-specific expert observations
int<lower=1> K_exp_dim; // Number of unique expert items
array[N_exp_dim] int<lower=1, upper=K_exp_dim> kk_exp_dim;
array[N_exp_dim] int<lower=1, upper=S> ss_exp_dim;
array[N_exp_dim] int<lower=1, upper=P> pp_exp_dim;
array[N_exp_dim] int<lower=0> val_dim_int; // V5: rounded sum = round(mean * K * n_scale)
array[N_exp_dim] int<lower=1> n_total_exp_dim; // V5: K * n_scale (total trials)
array[N_exp_dim] int<lower=1> n_experts_exp_dim; // V5: K (number of experts)
// Dimension index for expert data
array[N_exp_dim] int<lower=1, upper=2> dim_idx_exp; // 1=economic, 2=galtan
// V4: Constituent structure for expert dim observations
int<lower=1> N_const_exp_dim_total;
array[N_exp_dim] int<lower=1> n_const_exp_dim;
array[N_exp_dim] int<lower=1> const_offset_exp_dim;
array[N_const_exp_dim_total] int<lower=1, upper=R> const_rr_exp_dim;
// =========================================================================
// Expert general L-R data (V5: integer observations + scale size)
// =========================================================================
int<lower=1> N_exp_lr;
int<lower=1> K_exp_lr;
array[N_exp_lr] int<lower=1, upper=K_exp_lr> kk_exp_lr;
array[N_exp_lr] int<lower=1, upper=S> ss_exp_lr;
array[N_exp_lr] int<lower=1, upper=P> pp_exp_lr;
array[N_exp_lr] int<lower=0> val_lr_int; // V5: rounded sum = round(mean * K * n_scale)
array[N_exp_lr] int<lower=1> n_total_exp_lr; // V5: K * n_scale (total trials)
array[N_exp_lr] int<lower=1> n_experts_exp_lr; // V5: K (number of experts)
// V6: Decade indexing for hierarchical L-R weights
int<lower=1> D_lr; // Number of decades
array[N_exp_lr] int<lower=1, upper=D_lr> dd_exp_lr; // Decade index per LR obs
// V4: Constituent structure for expert L-R observations
int<lower=1> N_const_exp_lr_total;
array[N_exp_lr] int<lower=1> n_const_exp_lr;
array[N_exp_lr] int<lower=1> const_offset_exp_lr;
array[N_const_exp_lr_total] int<lower=1, upper=R> const_rr_exp_lr;
// Prior information
real mn_resp_log_man;
real mn_resp_log_exp_dim;
real mn_resp_log_exp_lr;
// Identification anchor
int<lower=1, upper=S> anchor_segment; // CDU segment (1375 with unions, 211 without)
// V3 compatibility: these are still passed but not used in V4+ likelihood
// (kept so older data dicts work without modification for backwards compat)
array[N_man] int<lower=1, upper=R> rr_man; // Segment-year index (unused in V4+ likelihood)
array[N_exp_dim] int<lower=1, upper=R> rr_exp_dim;
array[N_exp_lr] int<lower=1, upper=R> rr_exp_lr;
}
transformed data {
real eps = 1e-6;
real one_minus_eps = 1 - eps;
// Count zero and non-zero samples
int N_man_zero = 0;
int N_man_nonzero = 0;
for (n in 1:N_man) {
if (sample[n] == 0) {
N_man_zero += 1;
} else {
N_man_nonzero += 1;
}
}
// Create index arrays for zero/nonzero split
array[N_man_zero > 0 ? N_man_zero : 1] int idx_zero;
array[N_man_nonzero > 0 ? N_man_nonzero : 1] int idx_nonzero;
{
int pos_zero = 1;
int pos_nonzero = 1;
for (n in 1:N_man) {
if (sample[n] == 0) {
idx_zero[pos_zero] = n;
pos_zero += 1;
} else {
idx_nonzero[pos_nonzero] = n;
pos_nonzero += 1;
}
}
}
// V4: Pre-compute constituent info for nonzero manifesto obs
array[N_man_nonzero > 0 ? N_man_nonzero : 1] int kk_man_nonzero;
array[N_man_nonzero > 0 ? N_man_nonzero : 1] int orig_idx_nonzero;
array[N_man_nonzero > 0 ? N_man_nonzero : 1] int direction_nonzero;
array[N_man_nonzero > 0 ? N_man_nonzero : 1] int pp_man_nonzero;
array[N_man_nonzero > 0 ? N_man_nonzero : 1] int dim_idx_nonzero;
array[N_man_nonzero > 0 ? N_man_nonzero : 1] int n_const_nonzero;
array[N_man_nonzero > 0 ? N_man_nonzero : 1] int const_offset_nonzero;
{
for (i in 1:N_man_nonzero) {
int n = idx_nonzero[i];
orig_idx_nonzero[i] = n;
kk_man_nonzero[i] = kk_man[n];
direction_nonzero[i] = direction_man[n];
pp_man_nonzero[i] = pp_man[n];
dim_idx_nonzero[i] = dim_idx_man[n];
n_const_nonzero[i] = n_const_man[n];
const_offset_nonzero[i] = const_offset_man[n];
}
}
// Pre-compute segment start positions
array[S + 1] int segment_start;
segment_start[1] = 1;
for (s in 1:S) {
segment_start[s + 1] = segment_start[s] + len_theta_ts[s];
}
// Extract sample and positive for nonzero observations
array[N_man_nonzero > 0 ? N_man_nonzero : 1] int sample_nonzero;
array[N_man_nonzero > 0 ? N_man_nonzero : 1] int positive_nonzero;
for (i in 1:N_man_nonzero) {
sample_nonzero[i] = sample[idx_nonzero[i]];
positive_nonzero[i] = positive[idx_nonzero[i]];
}
// Pre-compute family-to-country mapping
array[F] int family_country;
{
for (f in 1:F) {
family_country[f] = 0;
}
for (s in 1:S) {
int f = segment_family[s];
if (family_country[f] == 0) {
family_country[f] = segment_country[s];
}
}
for (f in 1:F) {
if (family_country[f] == 0) {
family_country[f] = 1;
}
}
}
}
parameters {
// =========================================================================
// Latent position parameters - 2 dimensions
// =========================================================================
matrix[2, R] theta_ncp; // Non-centered: [1]=economic_lr, [2]=galtan
matrix[2, S] theta_init_raw; // Initial traits per segment
vector<lower=0>[2] sigma_theta_init; // SD of initial theta per dimension
// =========================================================================
// Three-level hierarchical variance
// =========================================================================
vector[2] mu_sigma_global_raw; // Global mean RW variance
vector<lower=0>[2] tau_sigma_country; // Country deviation scale
matrix[2, P] sigma_country_raw; // Non-centered country deviations
vector<lower=0>[2] tau_sigma_family; // Family deviation scale
matrix[2, F] sigma_family_raw; // Non-centered family deviations
// =========================================================================
// Country-item intercepts (coding convention differences)
// =========================================================================
matrix[P, K_man] country_item_raw;
real<lower=0> sigma_country_item;
// =========================================================================
// Zero-sample parameters
// =========================================================================
real alpha_zs;
vector[T_year] year_effect_raw;
real<lower=0> sigma_year_effect;
vector[S] segment_zs_raw;
real<lower=0> sigma_segment_zs;
vector[N_ciy] ciy_zs_raw;
real<lower=0> sigma_ciy_zs;
// =========================================================================
// Item parameters
// =========================================================================
// Text data: intercept + single positive loading (direction handled in data)
vector[K_man] gamma_man_intercept_raw;
vector<lower=0>[K_man] gamma_man_loading; // Positive loading
// Expert dimension-specific: intercept + slope (direct mapping to dimension)
vector[K_exp_dim] gamma_exp_intercept_raw;
vector<lower=0>[K_exp_dim] gamma_exp_slope;
// Expert general L-R: intercept + slope
vector[K_exp_lr] gamma_lr_intercept_raw;
vector<lower=0>[K_exp_lr] gamma_lr_slope;
// V6: Hierarchical L-R weights (replace simplex[2] lr_weights)
real lr_weight_global; // Global logit-scale weight
vector[P] lr_country_offset_raw; // Country offsets (non-centered)
vector[K_exp_lr] lr_source_offset_raw; // Source offsets (non-centered)
vector[D_lr] lr_decade_offset_raw; // Decade offsets (non-centered)
real<lower=0> sigma_lr_country; // SD of country offsets
real<lower=0> sigma_lr_source; // SD of source offsets
real<lower=0> sigma_lr_decade; // SD of decade offsets
// Precision parameters
real<lower=0> phi_exp_dim; // Precision for dimension-specific (now: only measurement noise)
real<lower=0> phi_exp_lr; // Precision for general L-R (now: only measurement noise)
// Scale parameters
real<lower=0> sigma_intercept_man;
real<lower=0> sigma_loading_man;
real<lower=0> sigma_intercept_exp;
real<lower=0> sigma_slope_exp;
real<lower=0> sigma_intercept_lr;
real<lower=0> sigma_slope_lr;
real mu_lambda_man;
real mu_lambda_exp_dim;
real mu_lambda_exp_lr;
}
transformed parameters {
matrix[2, R] theta; // [1]=economic_lr, [2]=galtan
matrix[2, S] theta_init;
// Three-level hierarchical variance (2D)
matrix[2, P] sigma_theta_country;
for (d in 1:2) {
for (p in 1:P) {
sigma_theta_country[d, p] = log1p_exp(
mu_sigma_global_raw[d] + tau_sigma_country[d] * sigma_country_raw[d, p]
);
}
}
matrix[2, F] sigma_theta_family;
for (d in 1:2) {
for (f in 1:F) {
int c = family_country[f];
sigma_theta_family[d, f] = log1p_exp(
log(sigma_theta_country[d, c]) + tau_sigma_family[d] * sigma_family_raw[d, f]
);
}
}
// Non-centered parameterization for theta_init
for (dim in 1:2) {
theta_init[dim, :] = sigma_theta_init[dim] * theta_init_raw[dim, :];
}
// Country-item intercepts
matrix[P, K_man] country_item_intercept = sigma_country_item * country_item_raw;
// Zero-sample components
vector[T_year] year_effect = sigma_year_effect * year_effect_raw;
vector[S] segment_zs = sigma_segment_zs * segment_zs_raw;
vector[N_ciy] ciy_zs = sigma_ciy_zs * ciy_zs_raw;
// Zero-sample probability
vector[N_man] zero_sample_logit = alpha_zs +
year_effect[year_for_man] +
segment_zs[ss_man] +
ciy_zs[ciy_idx];
vector[N_man] zero_sample_prob = inv_logit(zero_sample_logit);
zero_sample_prob = fmax(fmin(zero_sample_prob, one_minus_eps), eps);
// Construct theta using family-specific random walk variance (2D)
for (dim in 1:2) {
for (s in 1:S) {
int start = segment_start[s];
int Ts = len_theta_ts[s];
int fam = segment_family[s];
real sigma_s = sigma_theta_family[dim, fam];
theta[dim, start] = theta_init[dim, s] + sigma_s * theta_ncp[dim, start];
if (Ts > 1) {
theta[dim, start + 1 : start + Ts - 1] = theta[dim, start] +
cumulative_sum(sigma_s * theta_ncp[dim, start + 1 : start + Ts - 1]);
}
}
}
// Item parameters
vector[K_man] gamma_man_intercept = mu_lambda_man + sigma_intercept_man * gamma_man_intercept_raw;
vector[K_exp_dim] gamma_exp_intercept = mu_lambda_exp_dim + sigma_intercept_exp * gamma_exp_intercept_raw;
vector[K_exp_lr] gamma_lr_intercept = mu_lambda_exp_lr + sigma_intercept_lr * gamma_lr_intercept_raw;
}
model {
// =========================================================================
// PRIORS
// =========================================================================
// Three-level hierarchical variance priors (2D)
mu_sigma_global_raw ~ normal(-0.8, 0.5);
tau_sigma_country ~ normal(0, 0.3);
to_vector(sigma_country_raw) ~ std_normal();
tau_sigma_family ~ normal(0, 0.2);
to_vector(sigma_family_raw) ~ std_normal();
// Other variance priors
sigma_theta_init ~ normal(0, 0.5);
to_vector(theta_init_raw) ~ std_normal();
to_vector(theta_ncp) ~ std_normal();
// Country-item intercept priors
to_vector(country_item_raw) ~ std_normal();
sigma_country_item ~ normal(0, 0.3);
// Zero-sample priors
alpha_zs ~ normal(-1, 1);
year_effect_raw ~ std_normal();
sigma_year_effect ~ normal(0, 0.5);
segment_zs_raw ~ std_normal();
sigma_segment_zs ~ normal(0, 0.3);
ciy_zs_raw ~ std_normal();
sigma_ciy_zs ~ normal(0, 0.3);
// =========================================================================
// Item parameter priors
// =========================================================================
// Text data item priors
mu_lambda_man ~ normal(mn_resp_log_man, 0.5);
sigma_intercept_man ~ normal(0, 1);
sigma_loading_man ~ normal(0, 0.5);
gamma_man_intercept_raw ~ std_normal();
gamma_man_loading ~ normal(1.0, sigma_loading_man);
// Expert dimension item priors
mu_lambda_exp_dim ~ normal(mn_resp_log_exp_dim, 0.5);
sigma_intercept_exp ~ normal(0, 1);
sigma_slope_exp ~ normal(0, 0.5);
gamma_exp_intercept_raw ~ std_normal();
gamma_exp_slope ~ normal(1.0, sigma_slope_exp);
// Expert L-R item priors
mu_lambda_exp_lr ~ normal(mn_resp_log_exp_lr, 0.5);
sigma_intercept_lr ~ normal(0, 1);
sigma_slope_lr ~ normal(0, 0.5);
gamma_lr_intercept_raw ~ std_normal();
gamma_lr_slope ~ normal(1.0, sigma_slope_lr);
// V6: Hierarchical L-R weight priors
lr_weight_global ~ normal(0, 1);
lr_country_offset_raw ~ std_normal();
lr_source_offset_raw ~ std_normal();
lr_decade_offset_raw ~ std_normal();
sigma_lr_country ~ normal(0, 0.5);
sigma_lr_source ~ normal(0, 0.5);
sigma_lr_decade ~ normal(0, 0.5);
// Expert data precision priors
phi_exp_dim ~ gamma(50, 0.5);
phi_exp_lr ~ gamma(10, 0.5);
// =========================================================================
// CDU ANCHOR CONSTRAINT
// With unions: anchors CDU (1375) at moderate center-right position
// Without unions: anchors CDU/CSU (211) as before
// =========================================================================
target += normal_lpdf(theta_init[1, anchor_segment] | 0.2, 0.2); // economic_lr
target += normal_lpdf(theta_init[2, anchor_segment] | 0.2, 0.2); // galtan
// =========================================================================
// LIKELIHOOD 1: Text data (binomial with zero-inflation)
// V4: Mean-constituent averaging for union observations
// =========================================================================
// Zero-sample observations
if (N_man_zero > 0) {
target += sum(log(zero_sample_prob[idx_zero]));
}
// Non-zero observations with constituent averaging
if (N_man_nonzero > 0) {
vector[N_man_nonzero] lin_man;
for (i in 1:N_man_nonzero) {
int nc = n_const_nonzero[i];
int off = const_offset_nonzero[i];
int dim = dim_idx_nonzero[i];
real avg_pos;
if (nc == 1) {
// Fast path: single party (>95% of observations)
avg_pos = theta[dim, const_rr_man[off]];
} else {
// Union: average over constituent thetas
avg_pos = 0;
for (c in 0:(nc-1)) {
avg_pos += theta[dim, const_rr_man[off + c]];
}
avg_pos /= nc;
}
lin_man[i] = gamma_man_intercept[kk_man_nonzero[i]] +
direction_nonzero[i] * gamma_man_loading[kk_man_nonzero[i]] * avg_pos +
country_item_intercept[pp_man_nonzero[i], kk_man_nonzero[i]];
}
for (i in 1:N_man_nonzero) {
target += log1m(zero_sample_prob[orig_idx_nonzero[i]]) +
binomial_logit_lpmf(positive_nonzero[i] | sample_nonzero[i], lin_man[i]);
}
}
// =========================================================================
// LIKELIHOOD 2: Expert dimension-specific data (V5: beta-binomial likelihood)
// V4: Constituent averaging for union-level expert obs
// =========================================================================
{
vector[N_exp_dim] pos;
for (n in 1:N_exp_dim) {
int nc = n_const_exp_dim[n];
int off = const_offset_exp_dim[n];
int dim = dim_idx_exp[n];
if (nc == 1) {
pos[n] = theta[dim, const_rr_exp_dim[off]];
} else {
pos[n] = 0;
for (c in 0:(nc-1)) {
pos[n] += theta[dim, const_rr_exp_dim[off + c]];
}
pos[n] /= nc;
}
}
vector[N_exp_dim] lin_exp_dim;
for (n in 1:N_exp_dim) {
lin_exp_dim[n] = gamma_exp_intercept[kk_exp_dim[n]] +
gamma_exp_slope[kk_exp_dim[n]] * pos[n];
}
vector[N_exp_dim] mu_exp_dim = inv_logit(lin_exp_dim);
mu_exp_dim = fmax(fmin(mu_exp_dim, one_minus_eps), eps);
// K-scaling: phi * K corrects for independent expert perceptions
vector[N_exp_dim] alpha_exp_dim = phi_exp_dim * to_vector(n_experts_exp_dim) .* mu_exp_dim;
vector[N_exp_dim] beta_exp_dim = phi_exp_dim * to_vector(n_experts_exp_dim) .* (1 - mu_exp_dim);
val_dim_int ~ beta_binomial(n_total_exp_dim, alpha_exp_dim, beta_exp_dim);
}
// =========================================================================
// LIKELIHOOD 3: Expert general L-R data (V6: hierarchical per-obs weights)
// V4: Constituent averaging + weighted combination of both dimensions
// V6: Per-observation weights via country + source + decade offsets
// =========================================================================
{
// V6: Compute per-observation economic weight on logit scale
vector[N_exp_lr] logit_w;
for (n in 1:N_exp_lr) {
logit_w[n] = lr_weight_global
+ sigma_lr_country * lr_country_offset_raw[pp_exp_lr[n]]
+ sigma_lr_source * lr_source_offset_raw[kk_exp_lr[n]]
+ sigma_lr_decade * lr_decade_offset_raw[dd_exp_lr[n]];
}
vector[N_exp_lr] w_econ = inv_logit(logit_w);
vector[N_exp_lr] combined_pos;
for (n in 1:N_exp_lr) {
int nc = n_const_exp_lr[n];
int off = const_offset_exp_lr[n];
if (nc == 1) {
int r = const_rr_exp_lr[off];
combined_pos[n] = w_econ[n] * theta[1, r] + (1 - w_econ[n]) * theta[2, r];
} else {
// Average the combined position across constituents
combined_pos[n] = 0;
for (c in 0:(nc-1)) {
int r = const_rr_exp_lr[off + c];
combined_pos[n] += w_econ[n] * theta[1, r] + (1 - w_econ[n]) * theta[2, r];
}
combined_pos[n] /= nc;
}
}
vector[N_exp_lr] lin_exp_lr;
for (n in 1:N_exp_lr) {
lin_exp_lr[n] = gamma_lr_intercept[kk_exp_lr[n]] +
gamma_lr_slope[kk_exp_lr[n]] * combined_pos[n];
}
vector[N_exp_lr] mu_exp_lr = inv_logit(lin_exp_lr);
mu_exp_lr = fmax(fmin(mu_exp_lr, one_minus_eps), eps);
// K-scaling: phi * K corrects for independent expert perceptions
vector[N_exp_lr] alpha_exp_lr = phi_exp_lr * to_vector(n_experts_exp_lr) .* mu_exp_lr;
vector[N_exp_lr] beta_exp_lr = phi_exp_lr * to_vector(n_experts_exp_lr) .* (1 - mu_exp_lr);
val_lr_int ~ beta_binomial(n_total_exp_lr, alpha_exp_lr, beta_exp_lr);
}
}
generated quantities {
// =========================================================================
// Direct outputs (no derivation needed)
// =========================================================================
// Two bipolar scales on [0,1] via inv_logit
vector[R] economic_lr = inv_logit(to_vector(theta[1, :])); // 0=left, 1=right
vector[R] galtan = inv_logit(to_vector(theta[2, :])); // 0=GAL, 1=TAN
// General left-right combining both dimensions (using global weight only)
// Per-R country/source/decade info not available in GQ, so use global mean
real lr_w_econ_global = inv_logit(lr_weight_global);
vector[R] general_lr = inv_logit(
lr_w_econ_global * to_vector(theta[1, :]) + (1 - lr_w_econ_global) * to_vector(theta[2, :])
);
// V6: Expose hierarchical L-R weight diagnostics
real lr_weight_econ_global = lr_w_econ_global;
real lr_sigma_country = sigma_lr_country;
real lr_sigma_source = sigma_lr_source;
real lr_sigma_decade = sigma_lr_decade;
// Expose hierarchical variance diagnostics (2D)
vector[2] mean_sigma_global;
vector[2] mean_sigma_country;
vector[2] mean_sigma_family;
for (d in 1:2) {
mean_sigma_global[d] = log1p_exp(mu_sigma_global_raw[d]);
mean_sigma_country[d] = mean(sigma_theta_country[d, :]);
mean_sigma_family[d] = mean(sigma_theta_family[d, :]);
}
}