Vote Predict
Thin scenario wrapper for opinion/vote simulation. Unlike product-feedback which scores 1-10, vote-predict uses categorical choices and post-stratification so the distribution maps to population-level prediction.
Recipe
import sys, json
sys.path.insert(0, str(__import__('pathlib').Path.home() / '.claude/skills/persona-sim'))
from lib import sampler, ipf, aggregator
from lib.sim_engine import SYSTEM_PROMPT, _persona_card
from lib.llm_router import generate
# 1. Sample a large panel (census-matched after IPF)
panel = sampler.sample_personas(n=100, source="nemotron_usa", mode="stream")
# 2. Compute IPF weights to match target population marginals
weights = ipf.ipf_weights(
panel,
targets={
"age": {"<25": 0.12, "25-39": 0.26, "40-59": 0.33, "60+": 0.29}, # US adult
"gender": {"male": 0.49, "female": 0.51},
},
bucketers={"gender": lambda x: x.strip().lower() if isinstance(x, str) else None},
)
# 3. Ask each persona the question
def ask(persona, question, options):
task = (f"{question}\nChoose ONE of: {options}.\n"
f'Respond JSON: {{"vote": "<choice>"}}')
resp = generate(system=SYSTEM_PROMPT, persona_card=_persona_card(persona),
task=task, tier="default", max_tokens=100)
# parse JSON (see eval/run_eval._parse_json_answer)
...
# 4. Aggregate with weights
# Option-wise: weighted_share[option] = sum(weights[i] for i where vote[i]==option) / sum(weights)
Core rules
- NEVER output a single "winner" percentage as the answer. Output the full distribution + margin of uncertainty.
- Always apply IPF weights when the base panel doesn't match the target population (almost always for Nemotron).
- Report segment breakdowns (age × vote, education × vote) — even if the topline says 52/48, the story is in the segments.
- Attach bias audit warning from
lib/bias_audit.py — humans show acquiescence and framing biases that LLM personas do not. Flag the prediction as "LLM-synthetic, not a replacement for real polling".
- Flag multi-modal results — if
aggregator._dip_test_proxy says multi-modal, the population is split and averaging misleads.
Calibration priors
Known US baselines from eval/gss_20q.json can sanity-check predictions. If your simulated distribution is >0.2 JS-divergence from the reference for a similar question, the simulation is not trustworthy for this topic. Run eval first.
Do NOT use this skill for
- Real election forecasting (use prediction markets + polling aggregators)
- High-stakes policy decisions on single outcome (use this for hypothesis generation only)
- Issues where real-world events have shifted distributions after the Nemotron training cutoff (2024 or earlier)
See also
persona-sim/lib/ipf.py — post-stratification implementation
persona-sim/lib/bias_audit.py — run before publishing any prediction
persona-sim/eval/gss_20q.json — baseline attitude distributions
1---2name: vote-predict3description: Predict how a target population would vote, respond to a policy, or react to a political message. Use when the user wants a distribution (not a winner-take-all answer) across demographic segments, with calibration disclaimers. Triggers on 投票预测, 民意模拟, policy response, 政策反应, 选举模拟, 民意分布, 某群体怎么看.4---56# Vote Predict78Thin scenario wrapper for opinion/vote simulation. Unlike product-feedback which scores 1-10, vote-predict uses **categorical choices** and **post-stratification** so the distribution maps to population-level prediction.910## Recipe1112```python13import sys, json14sys.path.insert(0, str(__import__('pathlib').Path.home() / '.claude/skills/persona-sim'))15from lib import sampler, ipf, aggregator16from lib.sim_engine import SYSTEM_PROMPT, _persona_card17from lib.llm_router import generate1819# 1. Sample a large panel (census-matched after IPF)20panel = sampler.sample_personas(n=100, source="nemotron_usa", mode="stream")2122# 2. Compute IPF weights to match target population marginals23weights = ipf.ipf_weights(24 panel,25 targets={26 "age": {"<25": 0.12, "25-39": 0.26, "40-59": 0.33, "60+": 0.29}, # US adult27 "gender": {"male": 0.49, "female": 0.51},28 },29 bucketers={"gender": lambda x: x.strip().lower() if isinstance(x, str) else None},30)3132# 3. Ask each persona the question33def ask(persona, question, options):34 task = (f"{question}\nChoose ONE of: {options}.\n"35 f'Respond JSON: {{"vote": "<choice>"}}')36 resp = generate(system=SYSTEM_PROMPT, persona_card=_persona_card(persona),37 task=task, tier="default", max_tokens=100)38 # parse JSON (see eval/run_eval._parse_json_answer)39 ...4041# 4. Aggregate with weights42# Option-wise: weighted_share[option] = sum(weights[i] for i where vote[i]==option) / sum(weights)43```4445## Core rules46471. **NEVER output a single "winner" percentage as the answer.** Output the full distribution + margin of uncertainty.482. **Always apply IPF weights** when the base panel doesn't match the target population (almost always for Nemotron).493. **Report segment breakdowns** (age × vote, education × vote) — even if the topline says 52/48, the story is in the segments.504. **Attach bias audit warning** from `lib/bias_audit.py` — humans show acquiescence and framing biases that LLM personas do not. Flag the prediction as "LLM-synthetic, not a replacement for real polling".515. **Flag multi-modal results** — if `aggregator._dip_test_proxy` says multi-modal, the population is split and averaging misleads.5253## Calibration priors5455Known US baselines from `eval/gss_20q.json` can sanity-check predictions. If your simulated distribution is >0.2 JS-divergence from the reference for a similar question, **the simulation is not trustworthy for this topic**. Run eval first.5657## Do NOT use this skill for5859- Real election forecasting (use prediction markets + polling aggregators)60- High-stakes policy decisions on single outcome (use this for hypothesis generation only)61- Issues where real-world events have shifted distributions after the Nemotron training cutoff (2024 or earlier)6263## See also6465- `persona-sim/lib/ipf.py` — post-stratification implementation66- `persona-sim/lib/bias_audit.py` — run before publishing any prediction67- `persona-sim/eval/gss_20q.json` — baseline attitude distributions