#!/usr/bin/env julia ############################################################################# ## 02_data_loading.jl ## Load and preprocess data for latent trait model ## Loads three datasets: text_data (manifesto + PolDem), expert dimension-specific, expert general L-R ## ## Supports both: ## - 4D model (V10): type_high/type_low columns for bipolar bridges ## - 2D model (V1): dim_idx + direction columns for direct estimation ############################################################################# using DataFrames, CSV, CategoricalArrays, Statistics ############################################################################# ## UNION MAPPING: Individual party estimates via mean-constituent model ## Loads data/union_mapping.csv and builds lookup structures ############################################################################# """ load_union_mapping(project_root::String) Load union_mapping.csv and build lookup dictionaries. Returns (union_to_constituents, constituent_to_union) dicts. If file is missing or empty, returns empty dicts (backwards compatible). """ function load_union_mapping(project_root::String=".") mapping_file = joinpath(project_root, "data", "union_mapping.csv") union_to_constituents = Dict{Int, Vector{Int}}() constituent_to_union = Dict{Int, Int}() if !isfile(mapping_file) println(" No union_mapping.csv found - running without union decomposition") return union_to_constituents, constituent_to_union end df = CSV.read(mapping_file, DataFrame) if nrow(df) == 0 println(" union_mapping.csv is empty - running without union decomposition") return union_to_constituents, constituent_to_union end for row in eachrow(df) union_id = row.manifesto_pf_id expert_id = row.expert_pf_id if !haskey(union_to_constituents, union_id) union_to_constituents[union_id] = Int[] end if !(expert_id in union_to_constituents[union_id]) push!(union_to_constituents[union_id], expert_id) end constituent_to_union[expert_id] = union_id end println(" Union mapping loaded: $(length(union_to_constituents)) unions, $(length(constituent_to_union)) constituents") return union_to_constituents, constituent_to_union end ############################################################################# ## SEGMENT-BASED INDEXING CONFIGURATION ## Split parties at gaps > MAX_GAP years to avoid flat posteriors ############################################################################# const MAX_GAP = 7 # Maximum years between observations within a segment const MIN_OBS = 2 # Minimum observations per segment (drop segments with fewer) ############################################################################# ## 2D MODEL MAPPING CONFIGURATION ## Maps type_high/type_low pairs to dim_idx + direction ############################################################################# const TYPE_TO_DIM_DIRECTION = Dict( # Economic dimension: pro_market = right (+1), pro_welfare = left (-1) ("pro_market", "pro_welfare") => (dim_idx=1, direction=1), # Right ("pro_welfare", "pro_market") => (dim_idx=1, direction=-1), # Left # Cultural dimension: traditional = TAN (+1), cosmopolitan = GAL (-1) ("traditional", "cosmopolitan") => (dim_idx=2, direction=1), # TAN ("cosmopolitan", "traditional") => (dim_idx=2, direction=-1) # GAL ) # Expert dimension mapping (lrecon -> economic, galtan/cultural -> galtan) const EXPERT_VAR_TO_DIM = Dict( "lrecon_ches" => 1, "lrecon_vparty" => 1, "welf_vparty" => 1, "lrecon_gps" => 1, "lrecon_poppa" => 1, "galtan_ches" => 2, "libcon_gps" => 2, "immig_vparty" => 2, "lgbt_vparty" => 2, "culsup_vparty" => 2, "relig_vparty" => 2, "gender_vparty" => 2 ) function load_and_preprocess_4dim_data(start_year=1950; data_dir::String=".") println("Loading party-position data files...") println("Start year filter: $start_year") data_dir != "." && println("Data directory: $data_dir") # Load union mapping (check data_dir first, fall back to project root) println("\nLoading union mapping...") union_mapping_dir = isfile(joinpath(data_dir, "data", "union_mapping.csv")) ? data_dir : "." union_to_constituents, constituent_to_union = load_union_mapping(union_mapping_dir) # Load the three datasets text_data_raw = CSV.read(joinpath(data_dir, "text_data.csv"), DataFrame) expert_raw = CSV.read(joinpath(data_dir, "expert.csv"), DataFrame) lr_data_raw = CSV.read(joinpath(data_dir, "lr_data.csv"), DataFrame) # Filter to start year BEFORE calculating year0 text_data_raw = text_data_raw[text_data_raw.year .>= start_year, :] expert_raw = expert_raw[expert_raw.year .>= start_year, :] lr_data_raw = lr_data_raw[lr_data_raw.year .>= start_year, :] println("Data filtered to $start_year onwards:") println(" Text data (manifesto + PolDem): $(nrow(text_data_raw)) observations") println(" Expert: $(nrow(expert_raw)) observations") println(" L-R data: $(nrow(lr_data_raw)) observations") # Define base year for relative time indexing year0 = Int(minimum([minimum(text_data_raw.year), minimum(expert_raw.year), minimum(lr_data_raw.year)])) - 1 println("Base year set to: $year0") # Create type mapping for the four dimensions type_map = Dict( "pro_market" => 1, "pro_welfare" => 2, "cosmopolitan" => 3, "traditional" => 4 ) println("Type mapping: pro_market=1, pro_welfare=2, cosmopolitan=3, traditional=4") # Process text data (manifesto + PolDem media) text_data = copy(text_data_raw) text_data = text_data[text_data.year .> year0, :] # Add type indices for text items (V4/V10: bipolar bridge structure) if !("type_high" in names(text_data)) || !("type_low" in names(text_data)) error("Text data must contain 'type_high' and 'type_low' columns with values: pro_market, pro_welfare, cosmopolitan, traditional") end text_data.type_high_idx = [type_map[t] for t in text_data.type_high] text_data.type_low_idx = [type_map[t] for t in text_data.type_low] # V1 (2D model): Add dim_idx and direction columns # Maps type_high/type_low to single dimension + direction dim_idx_man = Int[] direction_man = Int[] for row in eachrow(text_data) key = (row.type_high, row.type_low) if haskey(TYPE_TO_DIM_DIRECTION, key) mapping = TYPE_TO_DIM_DIRECTION[key] push!(dim_idx_man, mapping.dim_idx) push!(direction_man, mapping.direction) else # Unknown mapping - this should not happen with valid data error("Unknown type_high/type_low pair: $(row.type_high) / $(row.type_low)") end end text_data.dim_idx_man = dim_idx_man text_data.direction_man = direction_man # Standard processing text_data.country = categorical(text_data.country) text_data.party = categorical(text_data.party) text_data.var = categorical(text_data.var) text_data.Year = Int.(text_data.year) .- year0 sort!(text_data, [:country, :party, :year, :var]) println("Text data processed: $(nrow(text_data)) observations with bipolar bridge structure") # Process expert dimension-specific data (bipolar items like lrecon_ches, galtan_ches) expert_dim_vars = ["lrecon_ches", "galtan_ches", "lrecon_vparty", "welf_vparty", "lrecon_gps", "libcon_gps", "lrecon_poppa", "immig_vparty", "lgbt_vparty", "culsup_vparty", "relig_vparty", "gender_vparty"] expert_dim = expert_raw[in.(expert_raw.var, Ref(expert_dim_vars)), :] expert_dim = expert_dim[(expert_dim.year .> year0) .& (expert_dim.val .>= 0) .& (expert_dim.val .<= 1), :] # V5: Load integer observations, scale sizes, and expert counts for beta-binomial likelihood expert_dim.val_int = Int.(expert_dim.val_int) expert_dim.n_scale = Int.(expert_dim.n_scale) expert_dim.n_experts = Int.(expert_dim.n_experts) # Add type mappings for dimension-specific expert data if !("type_low" in names(expert_dim)) || !("type_high" in names(expert_dim)) error("Expert data must contain 'type_low' and 'type_high' columns") end expert_dim.type_high_idx = [type_map[t] for t in expert_dim.type_high] expert_dim.type_low_idx = [type_map[t] for t in expert_dim.type_low] # V1 (2D model): Add dim_idx for expert dimension data dim_idx_exp = Int[] for row in eachrow(expert_dim) var_name = string(row.var) if haskey(EXPERT_VAR_TO_DIM, var_name) push!(dim_idx_exp, EXPERT_VAR_TO_DIM[var_name]) else # Fallback: infer from type_high/type_low key = (row.type_high, row.type_low) if haskey(TYPE_TO_DIM_DIRECTION, key) push!(dim_idx_exp, TYPE_TO_DIM_DIRECTION[key].dim_idx) else error("Unknown expert variable: $var_name with type pair $(row.type_high) / $(row.type_low)") end end end expert_dim.dim_idx_exp = dim_idx_exp expert_dim.country = categorical(expert_dim.country) expert_dim.party = categorical(expert_dim.party) expert_dim.var = categorical(expert_dim.var) expert_dim.Year = Int.(expert_dim.year) .- year0 sort!(expert_dim, [:country, :party, :year, :var]) println("Expert dimension-specific data processed: $(nrow(expert_dim)) observations") # Process expert general L-R data (cross-dimensional anchoring) lr_vars = ["lr_ches", "lr_poppa", "lr_morgan"] # General left-right items expert_lr = lr_data_raw[in.(lr_data_raw.var, Ref(lr_vars)), :] expert_lr = expert_lr[(expert_lr.year .> year0) .& (expert_lr.val .>= 0) .& (expert_lr.val .<= 1), :] # V5: Load integer observations, scale sizes, and expert counts for beta-binomial likelihood expert_lr.val_int = Int.(expert_lr.val_int) expert_lr.n_scale = Int.(expert_lr.n_scale) expert_lr.n_experts = Int.(expert_lr.n_experts) expert_lr.country = categorical(expert_lr.country) expert_lr.party = categorical(expert_lr.party) expert_lr.var = categorical(expert_lr.var) expert_lr.Year = Int.(expert_lr.year) .- year0 sort!(expert_lr, [:country, :party, :year, :var]) println("Expert general L-R data processed: $(nrow(expert_lr)) observations") # Validate data integrity println("\nData validation:") # Check text data dimension pair distribution (V4: bipolar bridges) type_pair_counts = combine(groupby(text_data, [:type_high, :type_low]), nrow => :count) for row in eachrow(type_pair_counts) println(" $(row.type_high) ↔ $(row.type_low): $(row.count) text data observations") end # Check expert dimension-specific type pairs type_pair_counts = combine(groupby(expert_dim, [:type_high, :type_low]), nrow => :count) for row in eachrow(type_pair_counts) println(" $(row.type_high) - $(row.type_low): $(row.count) expert dimension-specific observations") end # Check general L-R items lr_var_counts = combine(groupby(expert_lr, :var), nrow => :count) for row in eachrow(lr_var_counts) println(" $(row.var): $(row.count) general L-R observations") end # Check overlapping parties across datasets text_data_parties = Set(text_data.party) expert_dim_parties = Set(expert_dim.party) expert_lr_parties = Set(expert_lr.party) all_parties = union(text_data_parties, expert_dim_parties, expert_lr_parties) println("\nParty coverage:") println(" Total unique parties: $(length(all_parties))") println(" In text data: $(length(text_data_parties))") println(" In expert dimension-specific: $(length(expert_dim_parties))") println(" In expert general L-R: $(length(expert_lr_parties))") println(" In all three datasets: $(length(intersect(text_data_parties, expert_dim_parties, expert_lr_parties)))") return text_data, expert_dim, expert_lr, year0, union_to_constituents, constituent_to_union end # Execute if run directly if abspath(PROGRAM_FILE) == @__FILE__ text_data, expert_dim, expert_lr, year0, u2c, c2u = load_and_preprocess_4dim_data() println("4D data loading test completed successfully") end