From 7643658b15b3685b79e91a257fbebd968f619058 Mon Sep 17 00:00:00 2001 From: aseimel Date: Mon, 15 Jun 2026 15:18:30 +0200 Subject: [PATCH] Use cultural dimension labels --- README.md | 2 +- docs/union_mapping.md | 2 +- metadata/data_dictionary.csv | 8 ++++---- src/julia/01_run_model.jl | 6 +++--- src/julia/02_post_estimation.jl | 18 +++++++++--------- src/julia/validate_construct.jl | 6 +++--- src/julia/validate_convergent.jl | 22 +++++++++++----------- src/julia/validate_uncertainty.jl | 7 ++++--- 8 files changed, 36 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index c8736c9..6546233 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/docs/union_mapping.md b/docs/union_mapping.md index a1da918..1f35638 100644 --- a/docs/union_mapping.md +++ b/docs/union_mapping.md @@ -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. diff --git a/metadata/data_dictionary.csv b/metadata/data_dictionary.csv index a0167a5..582cff9 100644 --- a/metadata/data_dictionary.csv +++ b/metadata/data_dictionary.csv @@ -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, diff --git a/src/julia/01_run_model.jl b/src/julia/01_run_model.jl index e7df5ac..61ea7bf 100644 --- a/src/julia/01_run_model.jl +++ b/src/julia/01_run_model.jl @@ -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)") diff --git a/src/julia/02_post_estimation.jl b/src/julia/02_post_estimation.jl index a436fab..892f056 100644 --- a/src/julia/02_post_estimation.jl +++ b/src/julia/02_post_estimation.jl @@ -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:") diff --git a/src/julia/validate_construct.jl b/src/julia/validate_construct.jl index 6de32d1..15908e8 100644 --- a/src/julia/validate_construct.jl +++ b/src/julia/validate_construct.jl @@ -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 diff --git a/src/julia/validate_convergent.jl b/src/julia/validate_convergent.jl index 2d6cb49..0897f0a 100644 --- a/src/julia/validate_convergent.jl +++ b/src/julia/validate_convergent.jl @@ -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 diff --git a/src/julia/validate_uncertainty.jl b/src/julia/validate_uncertainty.jl index 14bbe65..165b2fd 100644 --- a/src/julia/validate_uncertainty.jl +++ b/src/julia/validate_uncertainty.jl @@ -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,