factcircuit
FactCircuit는 불변 출처와 단계별 검증, 제한된 검색, 재현 가능한 평가를 제공하는 감사 가능한 주장·증거 검증 루프입니다
비례대표제 하에서 미국 선거를 시뮬레이션하는 인터랙티브 체험, 45,707명의 CES 2024 설문 기반
An interactive simulation of US elections under proportional representation, built from 45,707 CES 2024 survey responses.
데이터HTML · Jupyter NotebookGitHub @braydeck온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Interactive simulation of US elections under proportional representation, built from 45,707 CES 2024 survey responses
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A full-stack simulation of what American politics might look like under proportional representation, using real ideological data from the 2024 Cooperative Election Study (CES): 60,000+ respondents, 45,707 after listwise deletion.
Methodology: docs/METHODOLOGY.md — how 45,707 survey responses become a ten-party legislature, with caveats.
For methodology, see docs/METHODOLOGY.md; for EFA factor loadings, docs/EFA_FACTORS.md; for which file is authoritative for a given number, docs/DATA_SOURCES.md. Agents start at CLAUDE.md.README 전문 보기 ↗A full-stack simulation of what American politics might look like under proportional representation, using real ideological data from the 2024 Cooperative Election Study (CES): 60,000+ respondents, 45,707 after listwise deletion.
▶ Explore the live simulation: https://usmultiparty.com/
Methodology:
docs/METHODOLOGY.md— how 45,707 survey responses become a ten-party legislature, with caveats. For methodology, seedocs/METHODOLOGY.md; for EFA factor loadings,docs/EFA_FACTORS.md; for which file is authoritative for a given number,docs/DATA_SOURCES.md. Agents start atCLAUDE.md.
This project explores what American politics might look like under different electoral rules:
The crossover field is hidden from the site. It is the least data-driven half of the model — the variants are synthetic positions (one axis shifted by ±25% of the inter-party SD) scored by factor-space proximity rather than by the DPGMM posterior everything else uses — and it never picked up the bootstrap, the turnout stops, or the ballot-depth variants, so it falls further behind the party-line side with each change.
Nothing is deleted: the pipeline scripts, the factor_deviation/ outputs, the JSON payloads, and every
crossover-only card in the viz still exist and still work. viz/src/constants/features.ts only removes the
affordances that reach them. Two ways back in:
?lab=crossover — read once at startup, so it survives tab navigation and lasts until reloadCROSSOVER_BY_DEFAULT to true in that file and rebuild, to restore it for everyoneRegenerating the field is unchanged — see the crossover block under Running the Pipeline.
data/processed/county_to_district.csv assigns each of the 3,142 counties to a multi-member
district, and it is an input to the House simulation — STV pools voters district by district.
draw_geographic_districts.py used to draw a different map on every run, because tie-breaks
resolved through set iteration order, which varies with PYTHONHASHSEED. That is fixed: the map is
byte-identical across runs, verified at seeds 0, 1 and 7.
The committed map predated the fix, so it could not be reproduced from the code. Resynced on 2026-07-31: the deterministic map is now the committed one, and everything downstream was rebuilt against it. 65 counties changed district — all within their own state and their own density tier, so no state's seat count moved — reshaping 31 of the 149 districts and reassigning three seats (Conservative +1, Nationalist +2, Labor −1, Populist −2). Six parties and the 873-seat total were unaffected.
pipeline/resync_district_map.py remains the driver for doing this again: six stages, dry-run by
default, --run to execute. Verified during the resync — senate, president and primary blocks in
uncertainty*.json came back byte-identical, which is the expected result since none of those
contests are district-pooled, and it confirms the bootstrap is reproducible given its seeds.
Measured cost: ~16 min per turnout stop, ~110 min for the seven, ~2 h end to end.
A robustness check that dissolved Solidarity (cluster 2) and let its voters' ballots flow to the remaining parties. Its UI toggle was added, removed, restored and removed again, and nothing in the app ever imported its payloads.
It is gone: the NO_STY=1 branches in pipeline/pure_only/, build_nosty_scenario(), the
pure_multi_nosty* output trees and the viz/src/data/archive/*NoSTY*.json payloads were all
deleted. To resurrect it, restore from history rather than re-adding an environment branch.
2024 CES Survey (N=45,707)
│
▼
Exploratory Factor Analysis (EFA)
24 policy items → 5 latent ideological dimensions (F1–F5)
│
▼
DPGMM Clustering
Factor scores → 10 voter typology clusters (C0–C9)
│
▼
Two candidate fields (used for House, Senate, and President)
• Party-line (pure_multi): 3 identical-platform candidates per party
• Crossover (factor_deviation): candidates shifted ±1 axis off base
│
├──────────────────────────────────────┐
▼ ▼
House STV Simulation Senate Simulation
873 seats / 180 districts 51 senators (one per state)
Droop quota + Gregory surplus STV primary → Condorcet + IRV
→ pure_multi/ + factor_deviation/ 4 scenarios (2 fields × 2 methods)
│ │
└──────────────┬───────────────────────┘
▼
Chamber Vote Model
38 bills (stv_config.BILL_VARS)
Sum-of-Binomials → P(pass)
Produced by Dirichlet Process Gaussian Mixture Model (DPGMM) clustering on 5 EFA factor scores. All 10 clusters are active parties, including OAO (cluster 7, the Order & Opportunity Party — the former "Blue Dogs," a law-and-order Democratic bloc kept as its own party). DISSOLVED_PARTIES = [].
| ID | Abbrev | Name | Character | ~Electorate |
|---|---|---|---|---|
| C0 | CON | Conservative | Mainstream center-right; pro-law enforcement, economically traditional | ~19% |
| C1 | SD | Social Democrat | Center-left institutionalist; supports safety net, moderate on social issues | ~16% |
| C2 | STY | Solidarity | Disaffected working-class left; skeptical of institutions, economically left but low union density | ~15% |
| C3 | NAT | Nationalist | Populist right; strong on immigration, anti-establishment, very high F5 | ~9% |
| C4 | LIB | Liberal | College-educated progressive; socially liberal, moderate-left economics | ~9% |
| C5 | POP | Populist | Right-of-center reformists; skeptical of elections, high F2 + F5 | ~11% |
| C6 | CUP | Civic Union Party | True centrists; cross-pressured, low electoral skepticism | ~10% |
| C7 | OAO | Order & Opportunity | Law-and-order Democrats (former "Blue Dogs"); a full party that wins seats | — |
| C8 | DSA | DSA | Progressive left; far left on security, high electoral skepticism | ~6% |
| C9 | PRG | Progressive | Progressive elite; urban, far left across most dimensions | ~5% |
House seat counts (party-line, canonical, 873 total): CON=202, LBR=158, STY=129, POP=104, LIB=93, CUP=89, NAT=43, DSA=25, OAO=15, PRG=15. Conservative is the largest party. Source: viz/src/data/houseSeats.json — see docs/DATA_SOURCES.md. (Do not quote clusterProfiles.json seatsHouse for seat counts — it's a population baseline, not seats won.)
Factor scores are standardized to the survey population (mean≈0, SD≈1). Absolute tier thresholds reflect position relative to the full electorate:
| Tier | Score Range |
|---|---|
| Very High | > +0.75 |
| High | +0.25 to +0.75 |
| Medium | −0.25 to +0.25 |
| Low | −0.75 to −0.25 |
| Very Low | < −0.75 |
High = pro-law enforcement, pro-border security, pro-surveillance Low = civil libertarian, anti-enforcement Top items: support increased police (+0.73), increase border patrols (+0.71), deny asylum (+0.66), oppose decreasing police (+0.65)
High = distrusts elections and government; anti-establishment Low = trusts institutions Top items: state/local elections not fair (+0.90), US elections not fair (+0.73) Anchored by the election-integrity items, but it also absorbs the government-trust items (CC24_423/424) and carries the real distrust signal — F2 correlates +0.84 with raw government distrust across the ten parties. Near-orthogonal to partisan ID — STY, POP, and DSA all score High despite being ideologically distant on F1/F5, so institutional distrust cuts across the left-right divide. (Formerly labeled "Electoral Skepticism.")
Top items: distrust federal govt (+0.66), distrust state govt (+0.48)
Non-interpretable residual; the label is misleading and the sign inverts. Re-deriving from the stored factor scores (repro: analysis/efa/exploration/verify_f3_inversion.py), a party's F3 score correlates −0.38 with its actual government distrust: the two most distrusting parties, POP and NAT, score lowest, while institutionalist CON scores highest among the right. The mechanism is that the government-trust items (CC24_423/424) load on both F2 and F3, and F2 — where the party-level variance sits — absorbs the real signal, so F3's discriminating variance instead comes from culturally-coded cross-loadings (immigration, race) and its ordering tracks a culturally-progressive residual, not distrust. F3 earns its k=5 slot only by keeping the trust variance split so the DPGMM can isolate CUP, the institutionalist that trusts both elections and government (see Robustness in docs/EFA_FACTORS.md). It is excluded from party-facing displays and flagged as a residual; the genuine distrust axis is F2. All 23 winning coalition types also score Medium on F3 (range −0.21 to +0.13), so it does not differentiate coalitions.
High = traditional religious values; conservative on abortion and same-sex marriage Low = secular, socially progressive Top items: church attendance (+0.69), abortion week limits (+0.69), oppose same-sex marriage recognition (+0.65)
High = populist-right; anti-immigration, fiscal conservatism, racial traditionalism Low = progressive-left Top items (negative-loaded): racial resentment (−0.62), oppose police reform (−0.56), oppose Dreamers (−0.54), oppose tax hike on $400k+ (−0.53) NAT is the extreme high end (+1.51); PRG (−0.99) and LIB (−0.95) are the extreme low end.
Institutional distrust reads as a floor, not a level. Clustering requires a complete answer on all 24 items, which restricts the analysis sample to the ~45,700 respondents who returned for the post-election wave. That attrition is not random: the returners run about eleven years older and markedly more politically engaged than the full 60,000, and the CES post-election weights correct the demographic margins but not the engagement gap. The exposure concentrates on one battery. The institutional-trust items, whether elections are run fairly and how far federal and state government can be trusted, are all post-wave, so the voters who left are absent from exactly the questions their attitudes would move most. And among those who did answer, the least engaged are the most distrustful: they score highest on election-illegitimacy and on distrust of state government. The alienated, low-information, "system is rigged" voter is the one least likely to have stayed to say so. So the simulation more likely understates institutional distrust than overstates it, especially toward elections and state government.
Broader modeling caveats (party cohesion, sincere voting, static factor space, turnout) live in docs/METHODOLOGY.md and the simulation's Caveats tab.
Scripts: pipeline/stv_main.py and supporting pipeline/stv_step1.py–pipeline/stv_step5.py
Published result: data/outputs/pure_multi/house/stv_seat_summary.csv → viz/src/data/houseSeats.json (the party-line view the viz shows).
Note: published seat counts come from pure_multi. The outdated 850-seat canonical/stv_seat_summary.csv was deleted 2026-09-12, along with three other CSVs in those directories that nothing read. The canonical* directories are retained for their inputs rather than their results: the pure_multi and factor_deviation runs read ballots_checkpoint.parquet + district_apportionment.csv, and the viz transfer matrix is built from canonical/transfer_matrix_10party.csv. See docs/DATA_SOURCES.md.
⌊ total_weight / (seats + 1) ⌋ + 1pre_dissolved); the baseline dissolves none. A dissolved party's ballots skip to the next-ranked active party.Seat results (party-line, canonical):
| Party | Seats | % | Urban | Suburban | Rural |
|---|---|---|---|---|---|
| CON | 202 | 23.1% | 107 | 67 | 28 |
| LBR | 158 | 18.1% | 89 | 51 | 18 |
| STY | 129 | 14.8% | 72 | 49 | 8 |
| POP | 104 | 11.9% | 52 | 37 | 15 |
| LIB | 93 | 10.7% | 63 | 24 | 6 |
| CUP | 89 | 10.2% | 47 | 31 | 11 |
| NAT | 43 | 4.9% | 16 | 16 | 11 |
| DSA | 25 | 2.9% | 15 | 8 | 2 |
| OAO | 15 | 1.7% | 9 | 4 | 2 |
| PRG | 15 | 1.7% | 11 | 3 | 1 |
This is the party-line field, the only one the site shows. The crossover field (run_fd_house_stv.py → data/outputs/factor_deviation/house/) runs the same STV over axis-shifted candidates; the House tab's Scenario toggle switches between the two fields, and appears only when the crossover flag is on.
Scripts: crossover field — pipeline/run_fd_senate_simulation.py; party-line field — pipeline/pure_only/run_pure_multi_senate.py (each runs both Condorcet and IRV)
Outputs: data/outputs/factor_deviation/senate/ and data/outputs/pure_multi/senate/ — senate_composition.csv (Condorcet) and senate_irv_composition.csv (IRV)
pure_multi) — 3 candidates per party, all on the identical party platform (e.g. STY_1). Tests proportional voting with parties as monoliths.factor_deviation) — 9 pure party candidates + 28 crossover candidates that shift one party off its base on a single axis: Security & Order, Anti-Establishment, or Populist Conservatism (±). Variant codes like STY_lo_ae (Solidarity, less anti-establishment).Senate seats by party across the four scenarios:
| Party | Crossover Cond | Crossover IRV | Party-line Cond | Party-line IRV |
|---|---|---|---|---|
| STY | 34 | 19 | 33 | 11 |
| SD | 11 | 27 | 15 | 26 |
| CON | 1 | 3 | 1 | 11 |
| POP | 5 | 1 | 1 | 2 |
| CUP | 0 | 1 | 1 | 0 |
| LIB | 0 | 0 | 0 | 1 |
NAT, DSA, and PRG win 0 senate seats in every scenario. Pattern: Condorcet rewards broad acceptability, so Solidarity (the spatial median) dominates; IRV rewards first-choice intensity, so Social Democrat leads and Conservative breaks through in the party-line field. Full state-by-state results are in comms/party_profiles.md.
Script: pipeline/chamber_vote_model.py
Outputs: data/outputs/senate/senate_vote_model.csv, data/outputs/house_vote_model.csv
For each of 37 binary CC24_ policy items, models the probability of a bill passing a floor vote.
Method — Sum-of-Independent-Binomials → Normal approximation:
E[Y] = Σᵢ nᵢ · pᵢ (expected yes votes)
σ²[Y] = Σᵢ nᵢ · pᵢ · (1 − pᵢ)
P(pass) = 1 − Φ((majority − 0.5 − E[Y]) / σ[Y]) (continuity correction)
where nᵢ = seats held by type i, pᵢ = that type's % supporting / 100.
Verdict thresholds: PASS ≥67% | TOSS-UP 33–67% | FAIL ≤33% Results (both chambers): 29 PASS / 1 TOSS-UP / 7 FAIL
These chambers are cross-cutting — they simultaneously pass both tax cuts AND tax hikes, both border enforcement AND Dreamer protections. Different majority coalitions form for each vote. This is expected behavior for proportional multi-party representation.
An earlier senate model elected blended senator types — CON/CUP, STY/LBR, SD/LIB and the
like — each a weighted mix of two clusters. The current senate elects pure party types only, so the
whole layer is gone from the live pipeline and from the app. Six scripts moved to
pipeline/archive/: run_senate_simulation.py and run_senate_irv.py (the mixed senate itself),
generate_blend_stats.py and senate_chamber_profile.py (its profiles), and
cross_chamber_coalitions.py and analyze_senate_ideology.py (the coalition-type diagnostics).
chamber_vote_model.py lost its senate half for the same reason.
Their outputs are deleted too: data/outputs/senate/, data/outputs/coalitions/ and
data/outputs/profiles/blend_stats.csv. generate_results.py was the only remaining reader — it
charted the mixed senate to an untracked results/ directory nothing links to — so it moved to the
archive with them.
All outputs are under data/outputs/.
| File | Description |
|---|---|
pure_multi/house/stv_seat_summary.csv | Party-line House seat totals by party + density tier (→ houseSeats.json) |
factor_deviation/house/stv_seat_summary.csv | Crossover House seat totals (→ fdHouseSeats.json) |
pure_multi/senate/senate_composition.csv · senate_irv_composition.csv | Party-line Senate — Condorcet / IRV winner per state |
factor_deviation/senate/senate_composition.csv · senate_irv_composition.csv | Crossover Senate — Condorcet / IRV winner per state |
canonical/{ballots_checkpoint.parquet, district_apportionment.csv} | Shared ballot + apportionment cache the pure_multi / factor_deviation runs read as input — see DATA_SOURCES.md |
canonical/transfer_matrix_10party.csv | Vote-transfer matrix (→ viz transferMatrix.json) |
profiles/cluster_stats.csv | Per-item statistics for all 10 clusters (policy + demographics) |
senate/senate_chamber_profile.csv · house_chamber_profile.csv | Seat-weighted policy aggregates per chamber (vote-model inputs) |
senate/senate_vote_model.csv · house_vote_model.csv | 37-item bill passage probability per chamber |
coalitions/coalition_type_profiles.csv (+ _factor_alignment, _pairwise) | Secondary cross-chamber factor-alignment diagnostic |
All scripts live in pipeline/ and use relative paths anchored to the project root.
All 10 clusters are active parties (C7 = Order and Opportunity Party). The election
sims + prepare_data.py are the reproducible path; they read data/processed/*.csv and
the geography checkpoint in data/outputs/canonical/ (party-independent).
# ── Base geography checkpoint (already cached; rebuilds ballots + apportionment) ──
python3 pipeline/stv_main.py
# ── Party-line field (pure_multi): ballots, primary, president, house, senate ──
python3 pipeline/pure_only/generate_pure_multi_ballots.py
python3 pipeline/pure_only/generate_party_ballots.py # single-candidate ballots (transfers)
python3 pipeline/pure_only/run_pure_multi_primary.py
python3 pipeline/pure_only/run_pure_multi_presidential.py
python3 pipeline/pure_only/run_pure_multi_house_stv.py # → pure_multi/house/
python3 pipeline/pure_only/run_pure_multi_house_stv.py --triple # → pure_multi_triple/house/
python3 pipeline/pure_only/run_pure_multi_senate.py # → pure_multi/senate/ (Condorcet + IRV)
# ── Crossover field (factor_deviation) ──────────────────────────────────────
python3 pipeline/generate_factor_deviation_candidates.py # OAO fields base only (small party)
python3 pipeline/generate_factor_deviation_ballots.py
python3 pipeline/generate_factor_deviation_stats.py # per-candidate policy support
python3 pipeline/run_fd_house_stv.py # → factor_deviation/house/
python3 pipeline/run_fd_house_stv.py --triple
python3 pipeline/run_fd_senate_simulation.py # → factor_deviation/senate/ (Condorcet + IRV)
python3 pipeline/run_fd_primary_2028.py
python3 pipeline/run_fd_irv_2028.py # FD presidential
# ── Canonical house + shared profiles ───────────────────────────────────────
python3 pipeline/run_house_canonical.py # → canonical/ (party-card seats)
python3 pipeline/house_chamber_profile.py # House chamber policy aggregate
# ── Respondent-level extras (read the raw .dta directly) ────────────────────
python3 pipeline/add_compare_items.py # FP/abortion/voting items → cluster_stats.csv
python3 pipeline/compute_intensity.py # full multi-point distributions → viz/src/data/clusterIntensity.json
# ── Visualization ──────────────────────────────────────────────────────────
cd viz && python3 scripts/prepare_data.py # Regenerate all JSON from outputs
cd .. && python3 pipeline/build_viz_bundles.py # REQUIRED after prepare_data — see below
cd viz && npm run dev # Dev server
npm run build # Production build → dist/
build_viz_bundles.pyis not optional. Nine derived artifacts sit downstream ofprepare_data.py: the depth bundles the presidency, primary and house tabs lazy-load, the rank-7 senate and legislation families that overwrite whatprepare_data.pyjust wrote, the single-race voter file, the top-two instructive-ballot splits, and the current-party profiles. Skip it and the app renders numbers from the previous run, or 404s a bundle and silently drops a whole section — nothing warns you. Run--listto see the order,--dry-runto see the commands. Every step is idempotent, so rerunning the whole thing is always safe.The two hex-tiling builders (
build_hex_ec_cartogram.py,build_hex_seat_cartogram.py) are deliberately not in it: the tilings key off apportionment rather than the per-run turnout and depth data, and the EC one needs one invocation per basis (--basis electoral,--basis population). Rebuild those by hand when seat counts change.
Legacy senate vote-model chain (
senate_chamber_profile.py→chamber_vote_model.py→generate_results.py, pluscross_chamber_coalitions.py) produces analysis CSVs and depends on the archived single-candidatepure_only/senate/run. It is not required for the viz:prepare_data.pyrecomputes every Legislation column (Party-Line, Crossover, Triple — House and Senate) directly from the native seat counts × per-cluster policy support, so those numbers reflect the current 10-party chambers without the old chain.
EFA/clustering use the full ordinal responses — the 0/1 collapse is display-only.
The typology is built from a polychoric correlation matrix (analysis/efa/pipeline/run_efa.py)
and Bartlett factor scores from the standardized full-scale item matrix
(analysis/efa/pipeline/efa_update.py): multi-point items enter at full resolution
(reverse-coding only flips direction; "Not sure" on the 1–3 trust items is mapped to the
scale midpoint, not dropped), so neutral/middle opinions are preserved in the factor scores
and cluster assignments. Binarization to a single "% agree"/"% increase" happens only in the
display layer (add_compare_items.py); compute_intensity.py recovers the full distributions
for the viz. (An earlier commit message on the Intensity view wrongly stated the clustering
ran on collapsed encodings — it does not.)
stv_main.py and generate_blend_stats.py were archived in an earlier cleanup and have
been restored to pipeline/ and re-keyed to the current party codes. The only piece still
archive-only is the single-candidate pure_only/senate/ run, which feeds the legacy
senate vote-model chain (see the note above) and is superseded by prepare_data.py's
recompute. Older archived copies under data/outputs/archive/ and pipeline/archive/
predate three party renames (Reform→Populist, Center→Civic Union, SD→Labor, C7→OAO) and
should not be restored as-is.
python3 >= 3.10
numpy
pandas
scipy
scikit-learn
pyreadstat # Stata .dta reading
pyarrow # Parquet I/O for ballot checkpoint
plotly # HTML cluster profile visualizations
openpyxl # Excel read for county density-tier map
| Dataset | Description | Path |
|---|---|---|
| 2024 CES | ~60,000 respondents, 2024 Cooperative Election Study | data/raw/2024 CES Base/CCES24_Common_OUTPUT_vv_topost_final.dta |
| Typology | DPGMM cluster assignments (45,707 rows) | data/processed/typology_cluster_assignments.csv |
| EFA scores | 5 factor scores per respondent | data/processed/efa_factor_scores.csv |
| Polychoric matrix | 24×24 polychoric correlation matrix | data/processed/polychoric_matrix.csv |
Analysis sample: N=45,707 after listwise deletion (24 items + commonpostweight non-missing).