Use cultural dimension labels

This commit is contained in:
aseimel
2026-06-15 15:18:30 +02:00
parent 9a16f3232f
commit 7643658b15
8 changed files with 36 additions and 35 deletions
+1 -1
View File
@@ -97,6 +97,6 @@ Original raw source files are not redistributed. Rebuilding inputs from raw file
The two position variables are scaled from 0 to 1:
- `economic_lr`: economic left to economic right.
- `galtan`: cosmopolitan/socially liberal to traditionalist/nationalist.
- `galtan`: cultural cosmopolitan--traditionalist position.
Column definitions are in `metadata/data_dictionary.csv`.
+1 -1
View File
@@ -33,7 +33,7 @@ The V4 Stan model (`stan_model_2dim_v4.stan`) uses these mappings to produce **i
3. **Expert data constrains individuals directly.** A CHES rating of CDU in 2019 maps to `theta[dim, rr_CDU_2019]` with no averaging. This is what identifies the *difference* between constituents.
4. **Identification depends on data availability:**
- **Periods with individual expert data** (e.g., CHES 1999-2024): Constituent estimates separate meaningfully. CSU appears more conservative on galtan than CDU, matching known ground truth.
- **Periods with individual expert data** (e.g., CHES 1999-2024): Constituent estimates separate meaningfully. CSU appears more traditionalist on the cultural dimension than CDU, matching known ground truth.
- **Periods without individual expert data** (e.g., 1950s-1990s): Only the shared manifesto constrains the mean. The random walk prior pulls constituents toward similar values, so CDU ≈ CSU with wide credible intervals on the gap. The estimates gradually differentiate as expert data appears.
5. **Backwards compatible.** With an empty `union_mapping.csv`, all observations have `n_const=1` and V4 reduces exactly to V3.
+4 -4
View File
@@ -9,13 +9,13 @@ party_2d_election_year_panel_vN.csv.gz,union_party_id,Union or alliance PartyFac
party_2d_election_year_panel_vN.csv.gz,in_union,Union membership indicator,Indicator that the row is associated with a union or alliance,boolean,0;1,0,1,,indicator,,Alliance mapping,,
party_2d_election_year_panel_vN.csv.gz,pervote,Vote share,Vote share at the election year,numeric,,0,100,,percent,,Election metadata,,
party_2d_election_year_panel_vN.csv.gz,economic_lr,Economic left-right posterior mean,Posterior mean of economic left-right position,numeric,,0,1,,unit interval,0=left; 1=right,Posterior draws,Mean after inverse-logit transformation,
party_2d_election_year_panel_vN.csv.gz,galtan,Cultural posterior mean,Posterior mean of cultural position,numeric,,0,1,,unit interval,0=cosmopolitan/socially liberal; 1=traditionalist/nationalist,Posterior draws,Mean after inverse-logit transformation,
party_2d_election_year_panel_vN.csv.gz,galtan,Cultural cosmopolitan--traditionalist posterior mean,Posterior mean of cultural cosmopolitan--traditionalist position,numeric,,0,1,,unit interval,0=cosmopolitan; 1=traditionalist,Posterior draws,Mean after inverse-logit transformation,
party_2d_election_year_panel_vN.csv.gz,economic_lr_se,Economic posterior standard error,Posterior standard deviation for economic_lr,numeric,,0,,,unit interval,,Posterior draws,Standard deviation over posterior draws,
party_2d_election_year_panel_vN.csv.gz,galtan_se,Cultural posterior standard error,Posterior standard deviation for galtan,numeric,,0,,,unit interval,,Posterior draws,Standard deviation over posterior draws,
party_2d_election_year_panel_vN.csv.gz,galtan_se,Cultural posterior standard error,Posterior standard deviation for the cultural estimate,numeric,,0,,,unit interval,,Posterior draws,Standard deviation over posterior draws,
party_2d_election_year_panel_vN.csv.gz,economic_lr_q025,Economic lower posterior interval,2.5 percent posterior quantile for economic_lr,numeric,,0,1,,unit interval,0=left; 1=right,Posterior draws,Quantile over posterior draws,
party_2d_election_year_panel_vN.csv.gz,economic_lr_q975,Economic upper posterior interval,97.5 percent posterior quantile for economic_lr,numeric,,0,1,,unit interval,0=left; 1=right,Posterior draws,Quantile over posterior draws,
party_2d_election_year_panel_vN.csv.gz,galtan_q025,Cultural lower posterior interval,2.5 percent posterior quantile for galtan,numeric,,0,1,,unit interval,0=cosmopolitan/socially liberal; 1=traditionalist/nationalist,Posterior draws,Quantile over posterior draws,
party_2d_election_year_panel_vN.csv.gz,galtan_q975,Cultural upper posterior interval,97.5 percent posterior quantile for galtan,numeric,,0,1,,unit interval,0=cosmopolitan/socially liberal; 1=traditionalist/nationalist,Posterior draws,Quantile over posterior draws,
party_2d_election_year_panel_vN.csv.gz,galtan_q025,Cultural lower posterior interval,2.5 percent posterior quantile for the cultural estimate,numeric,,0,1,,unit interval,0=cosmopolitan; 1=traditionalist,Posterior draws,Quantile over posterior draws,
party_2d_election_year_panel_vN.csv.gz,galtan_q975,Cultural upper posterior interval,97.5 percent posterior quantile for the cultural estimate,numeric,,0,1,,unit interval,0=cosmopolitan; 1=traditionalist,Posterior draws,Quantile over posterior draws,
party_2d_election_year_panel_vN.csv.gz,election_id,Election identifier,Election identifier where available,string,,,,,identifier,,Election metadata,,Missing where no election identifier is available
party_2d_election_year_panel_vN.csv.gz,election_date,Election date,Election date where available,date,,,,,date,,Election metadata,,Missing in current processed election metadata
party_2d_election_year_panel_vN.csv.gz,has_text,Text support indicator,Indicator for direct or nearby text evidence,boolean,0;1,0,1,,indicator,,Source-support construction,Nearby threshold documented in source_support_dictionary.csv,
1 file_name variable_name label description type allowed_values range_min range_max missing_value_code unit scale_direction constructed_from construction_rule notes
9 party_2d_election_year_panel_vN.csv.gz in_union Union membership indicator Indicator that the row is associated with a union or alliance boolean 0;1 0 1 indicator Alliance mapping
10 party_2d_election_year_panel_vN.csv.gz pervote Vote share Vote share at the election year numeric 0 100 percent Election metadata
11 party_2d_election_year_panel_vN.csv.gz economic_lr Economic left-right posterior mean Posterior mean of economic left-right position numeric 0 1 unit interval 0=left; 1=right Posterior draws Mean after inverse-logit transformation
12 party_2d_election_year_panel_vN.csv.gz galtan Cultural posterior mean Cultural cosmopolitan--traditionalist posterior mean Posterior mean of cultural position Posterior mean of cultural cosmopolitan--traditionalist position numeric 0 1 unit interval 0=cosmopolitan/socially liberal; 1=traditionalist/nationalist 0=cosmopolitan; 1=traditionalist Posterior draws Mean after inverse-logit transformation
13 party_2d_election_year_panel_vN.csv.gz economic_lr_se Economic posterior standard error Posterior standard deviation for economic_lr numeric 0 unit interval Posterior draws Standard deviation over posterior draws
14 party_2d_election_year_panel_vN.csv.gz galtan_se Cultural posterior standard error Posterior standard deviation for galtan Posterior standard deviation for the cultural estimate numeric 0 unit interval Posterior draws Standard deviation over posterior draws
15 party_2d_election_year_panel_vN.csv.gz economic_lr_q025 Economic lower posterior interval 2.5 percent posterior quantile for economic_lr numeric 0 1 unit interval 0=left; 1=right Posterior draws Quantile over posterior draws
16 party_2d_election_year_panel_vN.csv.gz economic_lr_q975 Economic upper posterior interval 97.5 percent posterior quantile for economic_lr numeric 0 1 unit interval 0=left; 1=right Posterior draws Quantile over posterior draws
17 party_2d_election_year_panel_vN.csv.gz galtan_q025 Cultural lower posterior interval 2.5 percent posterior quantile for galtan 2.5 percent posterior quantile for the cultural estimate numeric 0 1 unit interval 0=cosmopolitan/socially liberal; 1=traditionalist/nationalist 0=cosmopolitan; 1=traditionalist Posterior draws Quantile over posterior draws
18 party_2d_election_year_panel_vN.csv.gz galtan_q975 Cultural upper posterior interval 97.5 percent posterior quantile for galtan 97.5 percent posterior quantile for the cultural estimate numeric 0 1 unit interval 0=cosmopolitan/socially liberal; 1=traditionalist/nationalist 0=cosmopolitan; 1=traditionalist Posterior draws Quantile over posterior draws
19 party_2d_election_year_panel_vN.csv.gz election_id Election identifier Election identifier where available string identifier Election metadata Missing where no election identifier is available
20 party_2d_election_year_panel_vN.csv.gz election_date Election date Election date where available date date Election metadata Missing in current processed election metadata
21 party_2d_election_year_panel_vN.csv.gz has_text Text support indicator Indicator for direct or nearby text evidence boolean 0;1 0 1 indicator Source-support construction Nearby threshold documented in source_support_dictionary.csv
+3 -3
View File
@@ -5,7 +5,7 @@
## Executes the complete pipeline: data loading → preparation → model fitting
##
## Supports two model versions:
## - "2dim": 2D bipolar model (V1) - estimates economic_lr and galtan directly
## - "2dim": 2D bipolar model (V1) - estimates economic left-right and cultural cosmopolitan--traditionalist positions directly
## - "4dim": 4D unipolar model (V10) - estimates 4 traits, derives 2 scales
##
## Default is 2D model (better identification, faster convergence)
@@ -40,7 +40,7 @@ println(" Adapt delta: $(ADAPT_DELTA)")
println(" Max depth: $(MAX_DEPTH)")
println(" Start year: $(START_YEAR)")
if MODEL_VERSION == "2dim"
println("\n 2D MODEL: Estimates economic_lr and galtan directly")
println("\n 2D MODEL: Estimates economic left-right and cultural cosmopolitan--traditionalist positions directly")
println(" (Half the parameters, better convergence)")
else
println("\n 4D MODEL: Estimates 4 traits, derives 2 scales")
@@ -256,7 +256,7 @@ function run_model(;
println(" Expert dimension-specific observations: $(dat_4dim["N_exp_dim"])")
println(" Expert general L-R observations: $(dat_4dim["N_exp_lr"])")
if MODEL_VERSION == "2dim"
println(" Dimensions estimated: 2 (economic_lr, galtan)")
println(" Dimensions estimated: 2 (economic left-right, cultural cosmopolitan--traditionalist)")
println(" Theta parameters: $(2 * data_prep.R) (2 × R)")
else
println(" Dimensions estimated: 4 (pro_market, pro_welfare, cosmopolitan, traditional)")
+9 -9
View File
@@ -3,7 +3,7 @@
02_post_estimation.jl - Extract party position estimates from Stan model output
Supports both model versions:
- 2D model (V1): Extracts economic_lr and galtan directly
- 2D model (V1): Extracts economic left-right and cultural cosmopolitan--traditionalist positions directly
- 4D model (V10): Extracts 4 traits + 2 derived scales
V10/V1 UPDATE: Handles segment-based indexing
@@ -520,7 +520,7 @@ function extract_estimates(chains::DataFrame, segment_year_map::DataFrame, R::In
# Select quantities based on model version
if model_version == "2dim"
# 2D model: economic_lr and galtan are directly estimated
# 2D model: economic left-right and cultural cosmopolitan--traditionalist positions are directly estimated
# (general_lr is computed in Stan for anchoring but not extracted as output)
quantities = ["economic_lr", "galtan"]
test_col = "economic_lr.1"
@@ -703,7 +703,7 @@ function validate_output(output::DataFrame, segment_info::Union{DataFrame, Nothi
econ_marker = econ_ok ? "" : "*"
galtan_marker = galtan_ok ? "" : "*"
@printf(" %-15s econ=%.2f%s [%.2f-%.2f] galtan=%.2f%s [%.2f-%.2f] %s\n",
@printf(" %-15s economic=%.2f%s [%.2f-%.2f] cultural=%.2f%s [%.2f-%.2f] %s\n",
anchor.name, mean_econ, econ_marker, anchor.econ[1], anchor.econ[2],
mean_galtan, galtan_marker, anchor.galtan[1], anchor.galtan[2], status)
end
@@ -845,9 +845,9 @@ function save_output(output::DataFrame, metadata::Dict, segment_info::Union{Data
if is_2d
println(f, "Model: 2D Direct Bipolar")
println(f, "")
println(f, "Bipolar scales (0 = left/GAL, 1 = right/TAN):")
println(f, "Bipolar scales (0 = left/cosmopolitan, 1 = right/traditionalist):")
println(f, " economic_lr: Economic left-right position (directly estimated)")
println(f, " galtan: GAL-TAN cultural position (directly estimated)")
println(f, " galtan: Cultural cosmopolitan--traditionalist position (directly estimated)")
# Note: general_lr is computed internally for cross-dimensional anchoring
# but not reported as output (the two dimension-specific estimates are preferred)
else
@@ -856,12 +856,12 @@ function save_output(output::DataFrame, metadata::Dict, segment_info::Union{Data
println(f, "Dimensions (0 = low, 1 = high):")
println(f, " pro_market: Pro-market economic position")
println(f, " pro_welfare: Pro-welfare state position")
println(f, " cosmopolitan: Cosmopolitan/GAL cultural position")
println(f, " traditional: Traditional/TAN cultural position")
println(f, " cosmopolitan: Cosmopolitan cultural position")
println(f, " traditional: Traditionalist cultural position")
println(f, "")
println(f, "Derived bipolar scales (0 = left/GAL, 1 = right/TAN):")
println(f, "Derived bipolar scales (0 = left/cosmopolitan, 1 = right/traditionalist):")
println(f, " economic_lr: Economic left-right (derived from pro_market - pro_welfare)")
println(f, " galtan: GAL-TAN (derived from traditional - cosmopolitan)")
println(f, " galtan: Cultural cosmopolitan--traditionalist (derived from traditional - cosmopolitan)")
end
println(f, "")
println(f, "Uncertainty columns:")
+3 -3
View File
@@ -324,7 +324,7 @@ function validate_country_patterns(model::DataFrame)
sort!(country_stats, :n_obs, rev=true)
@printf("%-4s %6s %6s %8s %8s %8s %8s\n",
"CC", "Parties", "N", "Econ", "SD", "Galtan", "SD")
"CC", "Parties", "N", "Econ", "SD", "Cult", "SD")
println("-"^60)
for row in eachrow(country_stats[1:min(20, nrow(country_stats)), :])
@@ -349,9 +349,9 @@ function validate_country_patterns(model::DataFrame)
push!(issues, "right-skewed economy")
end
if row.mean_galtan < 0.35
push!(issues, "cultural-skewed")
push!(issues, "cosmopolitan-skewed")
elseif row.mean_galtan > 0.65
push!(issues, "TAN-skewed")
push!(issues, "traditionalist-skewed")
end
println(" $(row.country): $(join(issues, ", "))")
end
+11 -11
View File
@@ -201,13 +201,13 @@ function validate_economic_lr(model::DataFrame, expert::DataFrame)
end
function validate_galtan(model::DataFrame, expert::DataFrame)
"""Validate galtan against CHES galtan and V-Party/GPS libcons"""
"""Validate cultural cosmopolitan--traditionalist estimates against CHES and V-Party/GPS cultural measures"""
println("\n" * "="^60)
println("CONVERGENT VALIDITY: galtan")
println("CONVERGENT VALIDITY: cultural cosmopolitan--traditionalist")
println("="^60)
# Filter expert data for GAL-TAN dimension
# Filter expert data for cultural cosmopolitan--traditionalist dimension
galtan_vars = filter(v -> occursin("galtan", v) || occursin("libcon", v), unique(expert.var))
galtan_expert = filter(row -> row.var in galtan_vars, expert)
@@ -338,17 +338,17 @@ function validate_discriminant(model::DataFrame, expert::DataFrame)
@printf(" Model Economic × Expert Economic: r = %.3f (convergent, n=%d)\n", r, nrow(merged_ee))
end
# 2. Expert economic vs Model galtan (discriminant)
# 2. Expert economic vs model cultural dimension (discriminant)
merged_eg = innerjoin(econ_expert, model_gal, on=[:party, :year])
if nrow(merged_eg) >= 10
r = cor(merged_eg.val, merged_eg.galtan)
push!(results, (model_dim="galtan", expert_dim="economic",
r_pearson=r, r_spearman=corspearman(merged_eg.val, merged_eg.galtan),
n=nrow(merged_eg), type="discriminant"))
@printf(" Model GAL-TAN × Expert Economic: r = %.3f (discriminant, n=%d)\n", r, nrow(merged_eg))
@printf(" Model Cultural × Expert Economic: r = %.3f (discriminant, n=%d)\n", r, nrow(merged_eg))
end
# 3. Expert galtan vs Model galtan (convergent - already computed, include for matrix)
# 3. Expert cultural vs model cultural dimension (convergent - already computed, include for matrix)
galtan_vars = filter(v -> occursin("galtan", v) || occursin("libcon", v), unique(expert.var))
galtan_expert = filter(row -> row.var in galtan_vars, expert)
merged_gg = innerjoin(galtan_expert, model_gal, on=[:party, :year])
@@ -357,22 +357,22 @@ function validate_discriminant(model::DataFrame, expert::DataFrame)
push!(results, (model_dim="galtan", expert_dim="galtan",
r_pearson=r, r_spearman=corspearman(merged_gg.val, merged_gg.galtan),
n=nrow(merged_gg), type="convergent"))
@printf(" Model GAL-TAN × Expert GAL-TAN: r = %.3f (convergent, n=%d)\n", r, nrow(merged_gg))
@printf(" Model Cultural × Expert Cultural: r = %.3f (convergent, n=%d)\n", r, nrow(merged_gg))
end
# 4. Expert galtan vs Model economic (discriminant)
# 4. Expert cultural vs model economic dimension (discriminant)
merged_ge = innerjoin(galtan_expert, model_econ, on=[:party, :year])
if nrow(merged_ge) >= 10
r = cor(merged_ge.val, merged_ge.economic_lr)
push!(results, (model_dim="economic_lr", expert_dim="galtan",
r_pearson=r, r_spearman=corspearman(merged_ge.val, merged_ge.economic_lr),
n=nrow(merged_ge), type="discriminant"))
@printf(" Model Economic × Expert GAL-TAN: r = %.3f (discriminant, n=%d)\n", r, nrow(merged_ge))
@printf(" Model Economic × Expert Cultural: r = %.3f (discriminant, n=%d)\n", r, nrow(merged_ge))
end
println()
println("MTMM Matrix:")
println(" Expert Economic Expert GAL-TAN")
println(" Expert Economic Expert Cultural")
for r in results
if r.model_dim == "economic_lr" && r.expert_dim == "economic"
@printf(" Model Economic: %.3f ", r.r_pearson)
@@ -385,7 +385,7 @@ function validate_discriminant(model::DataFrame, expert::DataFrame)
end
for r in results
if r.model_dim == "galtan" && r.expert_dim == "economic"
@printf(" Model GAL-TAN: %.3f ", r.r_pearson)
@printf(" Model Cultural: %.3f ", r.r_pearson)
end
end
for r in results
+4 -3
View File
@@ -87,7 +87,7 @@ function load_chains_selective(run_dir::String, needed_rr::Set{Int}, K::Int)
# Build the set of column names we need
needed_cols = Set{String}()
# theta columns: economic_lr.{rr} and galtan.{rr} for each unique rr
# theta columns: economic_lr.{rr} and galtan.{rr} (cultural cosmopolitan--traditionalist) for each unique rr
for rr in needed_rr
push!(needed_cols, "economic_lr.$rr")
push!(needed_cols, "galtan.$rr")
@@ -384,6 +384,7 @@ function summarize_coverage(result::DataFrame, ci_level::Float64)
# Overall by dimension
dim_names = Dict(1 => "economic_lr", 2 => "galtan")
display_dim_names = Dict(1 => "economic left-right", 2 => "cultural cosmopolitan--traditionalist")
summary_rows = []
for dim in sort(unique(result.dim_idx))
@@ -394,8 +395,8 @@ function summarize_coverage(result::DataFrame, ci_level::Float64)
ci = wilson_ci(ppc, n)
dim_name = dim_names[dim]
println(@sprintf("\n %-15s: %.1f%% [%.1f%%, %.1f%%] (%d/%d)",
dim_name, 100*ppc, 100*ci.lower, 100*ci.upper, n_covered, n))
println(@sprintf("\n %-40s: %.1f%% [%.1f%%, %.1f%%] (%d/%d)",
display_dim_names[dim], 100*ppc, 100*ci.lower, 100*ci.upper, n_covered, n))
push!(summary_rows, (
dimension = dim_name,