kronikas
Principled election forecasting from opinion polls, powered by hierarchical Bayesian inference.
Most poll aggregators reduce rich, noisy data into a single point estimate and call it a day. kronikas does the opposite: it builds a full generative model of how public opinion evolves, learns each pollster's systematic biases, and propagates every source of uncertainty into honest probability distributions. The result is not just "Party A is at 42 %," but "Party A wins with 73 % probability, and here is the full distribution behind that number." If you are a political scientist, data journalist, election analyst, or anyone who needs defensible, reproducible forecasts from polling data, kronikas gives you a statistically rigorous engine you can trust, and customize, in a single pip install.
Why kronikas?
- 🎯 Probability over point estimates. Full posterior distributions and win probabilities, not just a number. Every forecast comes with calibrated uncertainty so you know exactly what you don't know.
- 🔧 Systematic bias correction. Automatically detects and corrects per-pollster house effects, cleanly separating genuine opinion shifts from firm-specific methodological bias.
- 📐 Structurally sound. Dirichlet observations and softmax constraints guarantee that predicted vote shares are always non-negative and sum to exactly 100 %. No ad-hoc normalization needed.
- ⚙️ Highly configurable. Flexible priors, per-pollster overrides, adjustable time grids, correlated random walks, and an escape hatch to any
pymc.sample()kwarg. Shape the model to your domain knowledge.
Used in production
kronikas is the forecasting engine behind százkilencvenkilenc.hu, where it powers live Hungarian election forecasts and tracks real-world polling shifts as they happen.
Not a programmer? Start here
Everything below assumes Python. If you would rather answer questions than write code, kronikas ships a guided workflow — a settings file in plain words, a one-command run, and a report written to be read rather than decoded.
pip install kronikas
kronikas skill install # hand the workflow to an AI assistant
kronikas skill install copies a skill into ~/.claude/skills/, after which
Claude Code (or any assistant you paste SKILL.md into) runs the whole thing
as a conversation: it asks what the election is, reads your poll file, asks
what you believe about pollster bias, runs the model, and explains the result.
kronikas skill path prints where the files live.
To drive it yourself, without an assistant:
-
Describe the race. Either fill in a browser form built from your own poll file —
kronikas form polls.csv --election-date 2026-04-12
which writes a self-contained page with a control for every party and pollster it found, and the settings file updating live as you click — or copy
forecast.template.yamland edit it. Every setting is a word, not a parameter:election: date: 2026-04-12 country: Hungary system: mixed # what "most votes" is actually worth polls: file: polls.csv beliefs: volatility: normal # calm | normal | volatile pollsters: Meridian: leans: {Progress: 1.5} # percentage points, + means overstates trust: low # high | normal | low industry_error: uncertainty_pp: 2.5 # how wrong every firm could be at once run: effort: standard # quick | standard | thorough
-
Check, then run.
kronikas guided forecast.yaml --check # validates and reads back in English kronikas guided forecast.yaml # fits the model
--checkcatches a misspelled party or pollster name — and suggests the right one — before you spend the sampling time, not after. -
Read the report. You get
report.html: a single self-contained page with win probabilities, forecast ranges, the trend with the polls behind it, house effects, the break-even polling error, and a model-health verdict in plain language. No internet connection to view it, nothing to install to share it.kronikas report <dir>/report_data.jsonrebuilds the page without refitting.
Word settings map onto the same ModelConfig documented below — volatility: volatile is sigma_walk_prior=0.10, trust: low is sigma_house=0.6 — and
an advanced: block passes anything through untouched, so the guided path is
a front door rather than a walled garden.
Installation
The fastest way to get started:
pip install kronikas
For development, with uv (recommended):
uv sync --group dev
Or with pip:
pip install -e ".[dev]"
Quick start
1. Prepare a CSV
Each row is one poll. Required columns: date, pollster, sample_size, plus one column per candidate with their support value (any scale; values are normalised to 100 %).
If undecided respondents are included, pass undecided_column="Undecided".
That column is excluded from the candidate shares and reduces the effective
sample size by the decided fraction; otherwise renormalising decided shares
would make the poll look more precise than it is.
date,pollster,sample_size,Alice,Bob,Carol
2024-01-15,PollCo,1000,45,40,10
2024-02-01,SurveyInc,1200,44,42,11
2024-02-15,PollCo,800,46,39,12
2024-03-01,SurveyInc,1500,43,43,10
2024-03-15,PollCo,1000,47,38,13
2. Run a forecast
from kronikas import ElectionForecast, ModelConfig
forecast = ElectionForecast(
polls_csv="polls.csv",
election_date="2024-11-05",
# today defaults to date.today(); override for reproducibility:
today="2024-03-20",
)
result = forecast.run()
print(result.summary())
3. Inspect the output
result is a ForecastResult with:
# Per-candidate estimates for today and election day
for est in result.today_estimates:
print(f"{est.name}: {est.mean:.1f}% (90% CI: {est.ci_lower:.1f}%-{est.ci_upper:.1f}%)")
# Plurality probabilities (election day): P(this candidate polls highest)
for name, prob in result.win_probabilities.items():
print(f"{name}: {prob:.1%}")
# Probability of clearing a vote-share threshold (e.g. an electoral threshold)
for name, prob in result.threshold_probabilities(5.0).items():
print(f"{name} reaches 5%: {prob:.1%}")
# Head-to-head comparison
print(result.lead_probability("Alice", "Bob"))
# Sampler convergence diagnostics
print(result.diagnostics.summary())
# Full ArviZ InferenceData for custom analysis
result.trace
What
win_probabilitiesmeasures. It is the probability of holding a plurality of the vote share — of polling higher than every other candidate on election day. Under electoral systems that are not simple national plurality (runoffs, district seat allocation, electoral colleges, coalition formation), that is not the probability of winning office. Treat it as a vote-share statistic and build any seat or office model on top ofparty_forecast_dataframe().
4. Party forecast as a DataFrame
party_forecast_dataframe() returns all posterior draws as a
pandas.DataFrame: one row per draw, one column per party named after
the party. Values are vote shares in percentage points.
# Posterior draws at the reference date (today)
df_today = result.party_forecast_dataframe(day="today")
# Posterior draws at election day
df_election = result.party_forecast_dataframe(day="election_day")
# df.columns == ["Alice", "Bob", "Carol"]
# df.shape == (num_draws * num_chains, num_candidates)
# Each row sums to 100.0 (percentage points)
Warmup iterations are never included. Only the post-tuning posterior draws that PyMC keeps after sampling.
Use these DataFrames for custom downstream analysis:
import matplotlib.pyplot as plt
# Histogram of Alice's election-day support
df_election["Alice"].plot.hist(bins=40, edgecolor="white")
plt.xlabel("Vote share (%)")
plt.title("Alice – election-day forecast")
plt.show()
# Correlations between parties
print(df_election.corr().round(2))
# Probability that Alice leads Bob on election day
p = (df_election["Alice"] > df_election["Bob"]).mean()
print(f"P(Alice > Bob) = {p:.1%}")
5. House effects as a DataFrame
house_effects_dataframe() returns all posterior draws of the per-pollster
house effects as a pandas.DataFrame. Each value is the percentage-point
deviation from a neutral equal-support baseline (all candidates at 1/K)
produced by that pollster's bias term. Positive values mean the pollster
over-estimates a candidate; negative values mean under-estimation. Within
each draw and pollster, values across all candidates sum to zero.
The DataFrame uses a two-level column MultiIndex: the outer level is the
pollster name and the inner level is the candidate name.
df_he = result.house_effects_dataframe()
# df_he.columns is a MultiIndex with levels ["pollster", "candidate"]
# df_he.shape == (num_draws * num_chains, num_pollsters * num_candidates)
# Inspect PollCo's bias for all candidates
print(df_he["PollCo"].mean())
# Plot the posterior distribution of PollCo's Alice bias
df_he["PollCo"]["Alice"].plot.hist(bins=40, edgecolor="white")
A RuntimeError is raised when the model was run with a single pollster
(house effects are not identifiable in that case).
6. Save a forecast and reload it later
Sampling takes minutes, so a run worth keeping should not have to be repeated.
save() writes the full posterior and the result's authoritative sample
matrices to netCDF; load() rebuilds an equivalent ForecastResult, including
any scenario created with assume_shared_bias().
result.save("forecast-2024-03-20.nc")
from kronikas import ForecastResult
restored = ForecastResult.load("forecast-2024-03-20.nc")
For publishing just the numbers, to_dict() returns a small JSON-serialisable
summary with no posterior draws attached:
import json
payload = result.to_dict(thresholds=[5.0])
print(json.dumps(payload, indent=2))
7. Customise priors and sampler
config = ModelConfig(
# --- Sampler ---
num_tune=2000, # warmup iterations per chain
num_draws=2000, # posterior draws per chain
num_chains=4, # independent MCMC chains
cores=4, # CPU cores for parallel sampling
target_accept=0.99, # higher = fewer divergences
init_method="adapt_full", # better for correlated posteriors
progressbar=True, # show progress bar
# --- Time grid ---
time_step_days=3, # finer time resolution
# --- Priors (logit scale) ---
sigma_walk_prior=0.03, # smoother trend, per `walk_reference_days`
sigma_house_prior=0.2, # tighter house-effect prior
initial_sigma=0.3, # tighter prior on initial support
kappa_log_sigma=0.3, # tighter poll-precision prior
# --- Escape hatch for any pymc.sample() kwarg ---
sampler_kwargs={"nuts_sampler": "nutpie"},
)
result = ElectionForecast(
polls_csv="polls.csv",
election_date="2024-11-05",
config=config,
).run()
CSV format
| Column | Type | Description |
|---|---|---|
date |
date string | Poll date (any format pandas.to_datetime can parse, or specify date_format). |
pollster |
string | Polling firm identifier. |
sample_size |
positive int | Number of respondents. |
| candidate columns | numeric | Raw support for each candidate (normalised internally). |
Column names for date, pollster, and sample_size can be overridden:
ElectionForecast(
polls_csv="polls.csv",
election_date="2024-11-05",
date_column="poll_date",
pollster_column="firm",
sample_size_column="n",
candidate_columns=["Dem", "Rep"], # explicit subset
date_format="%d/%m/%Y", # non-ISO dates
decimal=",", # European-style decimal separator
)
European-style CSVs
Some locales write numbers with a comma as the decimal point (e.g. 45,3
instead of 45.3). Use the decimal parameter to tell the reader which
character to treat as the decimal separator:
ElectionForecast(
polls_csv="polls_eu.csv",
election_date="2024-11-05",
decimal=",",
)
decimal defaults to "." and accepts any single character.
Polls already in memory
When the data comes from a database or an upstream cleaning step, skip the CSV round-trip:
from kronikas import ElectionForecast, polls_from_dataframe
forecast = ElectionForecast.from_dataframe(polls_frame, election_date="2024-11-05")
result = forecast.run()
# Or validate/normalise a frame on its own:
poll_data = polls_from_dataframe(polls_frame)
from_dataframe accepts the same column-name overrides as the constructor, and
never mutates the frame you pass it.
Command line
Installing the package puts a kronikas executable on your path, so a
scheduled job can produce machine-readable output without a Python wrapper:
# Human-readable summary
kronikas forecast polls.csv --election-date 2024-11-05
# JSON to a file, with threshold probabilities, quiet enough for cron
kronikas forecast polls.csv \
--election-date 2024-11-05 \
--threshold 5 --threshold 10 \
--json forecast.json \
--save-trace forecast.nc \
--quiet
# Report how fragile the call is to an industry-wide polling error
kronikas forecast polls.csv \
--election-date 2024-11-05 \
--shared-bias 2 --shared-bias 4
# Non-ISO dates, European decimals, renamed columns
kronikas forecast polls.csv \
--election-date 2024-11-05 \
--date-column poll_date --pollster-column firm --sample-size-column n \
--date-format "%d/%m/%Y" --decimal ,
# Score the model against a past election
kronikas backtest polls.csv \
--election-date 2024-11-05 \
--as-of 2024-08-01 --as-of 2024-10-01 \
--actual "Alice=48.2,Bob=47.1,Carol=4.7"
--shared-bias PP reports the forecast under an assumed industry-wide error of
PP points, moved from the front-runner to the runner-up; repeat it for several
sizes. JSON output also carries shared_bias_breakeven_pp, the smallest such
error that would erase the lead. See Shared polling error
for why this cannot be measured from the polls themselves.
kronikas forecast exits non-zero when the sampler reports a convergence
problem, so a scheduled run fails loudly instead of publishing bad numbers.
Run kronikas forecast --help for the full option list, including sampler
settings and CSV schema overrides.
Four further subcommands serve the guided workflow described above:
kronikas form builds a browser form for writing a settings file,
kronikas guided runs a forecast from one, kronikas report rebuilds the
HTML page from a finished run, and kronikas skill install installs the
assistant skill.
Backtesting
A forecast that has never been scored is an assertion, not a measurement.
backtest() replays a campaign: for each as-of date it discards every later
poll, refits, and records what the model would have said about election day
knowing only what was available then.
from datetime import date
from kronikas import backtest
report = backtest(
"polls.csv",
election_date=date(2024, 11, 5),
as_of_dates=[date(2024, 8, 1), date(2024, 9, 1), date(2024, 10, 1)],
actual={"Alice": 48.2, "Bob": 47.1, "Carol": 4.7},
)
print(report.summary())
report.to_dataframe() # tidy: one row per (as-of date, candidate)
report.metrics() # MAE, RMSE, mean CRPS, 90% interval hit rate, bias
actual accepts any scale — percentages, fractions, or raw vote counts — and
is normalised the same way polls are.
Two notes on reading the output. The interval hit rate is descriptive, not a calibration estimate from one election. Reliable coverage claims require many sufficiently independent elections. CRPS is the proper score for the full marginal predictive distributions. Bias is reported per candidate, never pooled — forecast and actual shares both sum to 100, so signed errors cancel exactly across candidates and a pooled mean would be identically zero.
Each as-of date costs one full MCMC fit, so lower num_draws for exploratory
runs.
Convergence diagnostics
Every result carries the headline sampler statistics, and a
ConvergenceWarning is raised when something looks wrong — so a script cannot
quietly go on to print confident-looking numbers from chains that never mixed.
result = forecast.run()
print(result.diagnostics.summary())
result.diagnostics.converged # False if R-hat, ESS, or divergences look bad
result.diagnostics.issues # problems detected
result.diagnostics.notes # caveats, e.g. R-hat needs >= 2 chains
Thresholds are configurable via ModelConfig(r_hat_threshold=..., ess_threshold=...).
A single-chain run reports converged=True with a note, not a failure: one
chain leaves convergence unverified rather than demonstrating a problem. Use
num_chains >= 2 to actually check it.
For model comparison, set compute_log_likelihood=True to populate the trace's
log_likelihood group for arviz.loo / arviz.waic:
config = ModelConfig(compute_log_likelihood=True)
result = ElectionForecast("polls.csv", "2024-11-05", config=config).run()
import arviz as az
print(az.loo(result.trace))
Shared polling error
A bias that every pollster shares is invisible to this model, and to any
model fitted to a single election's polls. The likelihood is exactly unchanged
by shifting the latent trend one way and all house effects the other, so no
quantity of polls or pollsters can measure it. Worse, sigma_house is learned
from how much pollsters differ from each other: when they all lean the same
way, the model concludes they are all accurate and passes their common error
into the forecast one-for-one — while reporting a narrower interval, because
it reads their agreement as precision.
On a synthetic dead heat (truth 45–45) where every pollster shaded 3 pp toward one side, the model reported 47.3 and put the probability of that side leading at 97.8 %, with a 90 % interval that excluded the truth.
House effects cannot show you this: they capture only the differences between pollsters, so their average is pinned near zero no matter how large the common error is. There is no diagnostic for it. What there is instead:
Ask what it would cost — no refit
result.assume_shared_bias({"Alice": 3.0}) # "polls overstate Alice by 3 pp"
result.shared_bias_breakeven() # smallest error that erases the lead
The ridge is what makes this legitimate: a shifted posterior fits the observed
polls exactly as well, so this reports a different point the data cannot rule
out. Offsets are in percentage points, positive meaning the polls over-state
that candidate. Candidates you do not name take up the remainder in proportion
to their support, so {"Alice": 3.0} moves Alice by a full 3 pp.
This is an approximation to a refit — on synthetic checks the mean matched to within 0.05 pp, though tail probabilities can differ by several points. For numbers you intend to publish, use the model term below.
Or build it into the model
from kronikas import ModelConfig, SharedBiasPrior
config = ModelConfig(
shared_bias=SharedBiasPrior(
mean={"Alice": 2.0, "Bob": -2.0}, # directional belief, in pp
sd={"Alice": 1.5, "Bob": 1.5}, # and how sure you are of it
default_sd=2.5, # for candidates not listed
)
)
Both the centre and the spread are yours to set, per candidate. There is no reason to assume the industry's error is symmetric about zero — that is exactly the assumption that produced the 97.8 % call above — so if you believe polls have historically understated a party, say so.
Non-zero sd values are calibrated as marginal percentage-point standard
deviations around the initial support vector. The errors are joint and
zero-sum; they are not independent candidate logits. A candidate configured
with zero direct error can still move indirectly because all shares must sum
to 100 %.
shared_bias defaults to None, which reproduces earlier behaviour exactly:
the industry is assumed collectively unbiased, with no uncertainty attached.
House effects always sum to zero across candidates and pollsters, so delta
means "this pollster relative to the industry". This keeps the latent trend
identified even when no shared-bias prior is configured.
The scale must be supplied, not learned. A hierarchical scale estimated from the same polls collapses toward zero for the very reason above, leaving the term inert. Take the number from historical polling error in comparable races — often around 2–3 pp — or estimate it from past election results, which is the only data that can identify it.
Model
The model has three components:
-
Latent support (logistic-normal random walk). Candidate proportions are parameterised as K-1 log-ratios relative to a reference candidate. These evolve as a Gaussian random walk on a discretised time grid (default: weekly steps). Softmax maps the log-ratios back to the probability simplex, guaranteeing non-negative shares that sum to 1.
The grid is anchored backwards from election day, so its final node falls exactly on the election date and the election-day forecast is not contaminated by an extra partial step of drift. The grid therefore starts on or just before the first poll.
The random-walk prior
sigma_walk_prioris expressed perwalk_reference_days(7 by default) rather than per time step, so changingtime_step_dayschanges the resolution of the trend without also changing how volatile the prior says it is. -
House effects. Each pollster gets a bias term in log-ratio space, drawn from a zero-sum Normal prior around
mu_house. House effects are strictly zero-sum constrained across both candidates and pollsters. This removes unidentifiable sampler dimensions and defines each effect relative to the polling-industry average. The meanmu_housedefaults to zero (no assumed direction of bias) but can be set per pollster and per candidate to encode prior knowledge about a specific pollster's lean. When only a single pollster is present, house effects are omitted (not identifiable). Per-pollster prior overrides can replace the hierarchicalsigma_housewith a fixed SD for individual pollsters (see Per-pollster priors). -
Dirichlet observations. Each poll is modelled as
Dirichlet(kappa_scale * sample_size * latent_proportions). The learntkappa_scaleabsorbs overdispersion beyond pure multinomial sampling (design effects, non-response, etc.). When per-pollsterkappa_log_sigmaoverrides are specified, each pollster receives its ownkappa_scale.
Non-centred parameterisation is used for the random walk to avoid divergences.
Configuration reference
All fields on ModelConfig with their defaults:
Sampler settings
| Parameter | Default | Description |
|---|---|---|
num_tune |
1500 | Warmup (tuning) iterations per chain |
num_draws |
1000 | Posterior draws per chain (total samples = draws × chains) |
num_chains |
2 | Independent MCMC chains (≥ 2 recommended for R-hat) |
cores |
None | CPU cores for parallel sampling (None = auto-detect) |
target_accept |
0.95 | NUTS target acceptance rate (0.90–0.99) |
random_seed |
42 | Reproducibility seed |
init_method |
"jitter+adapt_diag" |
NUTS initialisation ("adapt_diag", "adapt_full", …) |
progressbar |
True | Show progress bar during sampling |
compute_log_likelihood |
False | Store pointwise log-likelihood for arviz.loo / waic |
sampler_kwargs |
{} |
Extra kwargs forwarded to pymc.sample() |
Time discretisation
| Parameter | Default | Description |
|---|---|---|
time_step_days |
7 | Time-grid granularity in days (grid ends exactly on election day) |
Priors (logit / log-ratio scale)
| Parameter | Default | Description |
|---|---|---|
sigma_walk_prior |
0.05 | HalfNormal scale for random-walk SD, per walk_reference_days (~1 pp/week at 50 %) |
walk_reference_days |
7 | Calendar window sigma_walk_prior refers to; keeps implied volatility grid-invariant |
sigma_house_prior |
0.3 | HalfNormal scale for house-effect SD (~5 pp max bias) |
initial_sigma |
0.5 | Normal SD for initial latent support |
kappa_log_sigma |
0.5 | SD of log-normal prior on poll precision scaling factor |
r_hat_threshold |
1.01 | R-hat above this triggers a ConvergenceWarning |
ess_threshold |
400.0 | Minimum bulk ESS below which a ConvergenceWarning is raised |
correlated_walk |
False | Enables LKJ-correlated random walk innovations rather than independent ones |
lkj_eta |
2.0 | Shape parameter for LKJ matrix prior (used when correlated_walk=True) |
Per-pollster overrides
| Parameter | Default | Description |
|---|---|---|
pollster_priors |
{} |
Dict mapping pollster name to PollsterPrior (see below) |
shared_bias |
None | SharedBiasPrior for industry-wide polling error (see Shared polling error) |
Per-pollster priors
Use PollsterPrior to set different priors for individual pollsters. This
is useful when you have external knowledge about a pollster's reliability or
known biases.
from kronikas import ElectionForecast, ModelConfig, PollsterPrior
config = ModelConfig(
pollster_priors={
# PollCo has a known small bias, constrain its house effect
"PollCo": PollsterPrior(sigma_house=0.1),
# SurveyInc uses an online panel, allow more overdispersion
"SurveyInc": PollsterPrior(kappa_log_sigma=1.0),
},
)
result = ElectionForecast(
polls_csv="polls.csv",
election_date="2024-11-05",
config=config,
).run()
Each PollsterPrior field is optional; None (the default) inherits the
global value from ModelConfig:
| Field | Default | Description |
|---|---|---|
sigma_house |
None (uses sigma_house_prior) |
Fixed house-effect SD for this pollster in logit space. Lower = more trusted. |
kappa_log_sigma |
None (uses kappa_log_sigma) |
SD of log-normal prior on this pollster's precision scaling. Higher = allow more overdispersion. |
mu_house |
None (all zeros) | Dict mapping candidate name to expected bias in percentage points. Positive = over-estimates, negative = under-estimates. Omitted candidates default to 0 pp. Converted to logit space relative to that candidate's own support level, so the bias must not push it outside (0 %, 100 %). |
How it works:
- House effects: Pollsters with a
sigma_houseoverride use that value directly as the SD for their house-effect prior, bypassing the hierarchicalsigma_houseparameter. Pollsters without an override continue to share the learnt hierarchicalsigma_house. If all pollsters have overrides, the hierarchicalsigma_houseis omitted entirely. - Kappa (precision): When any pollster has a
kappa_log_sigmaoverride, the model switches from a single sharedkappa_logto per-pollsterkappa_logvalues. Pollsters without overrides use the globalkappa_log_sigmaas their prior SD. - Unknown names: Pollster names in
pollster_priorsthat don't match any pollster in the data trigger a warning and are ignored.
Setting prior means for pollster–party bias
Use mu_house when you have external knowledge that a pollster
systematically leans toward or against a specific candidate. Values are
in percentage points. Specify the expected bias directly. You only
need to list the candidates you want to set; the rest default to 0 pp.
from kronikas import ElectionForecast, ModelConfig, PollsterPrior
config = ModelConfig(
pollster_priors={
# PollCo is believed to over-estimate Alice by 3 pp
"PollCo": PollsterPrior(mu_house={"Alice": 3}),
# SurveyInc tends to under-estimate Bob by 4 pp; also allow a wider SD
"SurveyInc": PollsterPrior(
mu_house={"Bob": -4},
sigma_house=0.4,
),
# YouGov: set means for two candidates, keep default sigma
"YouGov": PollsterPrior(mu_house={"Alice": 2, "Bob": -2}),
},
)
result = ElectionForecast(
polls_csv="polls.csv",
election_date="2024-11-05",
config=config,
).run()
Pollsters without a mu_house entry keep the default zero mean; only
the pollsters you explicitly configure are affected. Values are converted
relative to each candidate's initial support level.
Lower-level API
For more control, use the building blocks directly:
load_polls() and polls_from_dataframe() return a PollData, which exposes
poll_dates, last_poll_date, and up_to(cutoff) — the last of these returns
a copy restricted to polls on or before a date, and is what the backtester uses
to reconstruct what was known at a past moment.
from kronikas import ModelConfig, load_polls
from kronikas.model import build_model, run_inference, extract_results
from datetime import date
poll_data = load_polls("polls.csv")
poll_data.last_poll_date # most recent poll
earlier = poll_data.up_to(date(2024, 8, 1)) # only polls up to that date
config = ModelConfig(num_draws=500)
model, metadata = build_model(poll_data, date(2024, 11, 5), date.today(), config)
trace = run_inference(model, config)
result = extract_results(trace, poll_data, metadata)
# Direct access to ArviZ trace
import arviz as az
az.summary(result.trace)
az.plot_trace(result.trace, var_names=["sigma_walk", "kappa_log"])
Contributing
We welcome contributions of all kinds: bug reports, feature ideas, documentation improvements, and code. Whether you're fixing a typo or building a new feature, we'd love to have you involved.
👉 See CONTRIBUTING.md for setup instructions, coding guidelines, and how to submit a pull request.
Citation
If you use kronikas in your research, please cite it:
@software{Tisza_kronikas_2026,
author = {Tisza, Viktor},
title = {kronikas},
month = {3},
year = {2026},
publisher = {Zenodo},
version = {0.1.2},
doi = {10.5281/zenodo.19163741},
url = {https://github.com/vtisza/kronikas}
}
License
This project is licensed under the Apache License 2.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kronikas-0.2.0.tar.gz.
File metadata
- Download URL: kronikas-0.2.0.tar.gz
- Upload date:
- Size: 227.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72ca24cfaef0736f4216473c4b3205f4071fe75d688c715b082657d3d7013b4d
|
|
| MD5 |
fe2ea2b06e4b66c93f84d4101fe784e9
|
|
| BLAKE2b-256 |
ab0099835bf74471fc5dbb397874048d01bc74ca51daa2a1e728ba9b6fe6f5e5
|
Provenance
The following attestation bundles were made for kronikas-0.2.0.tar.gz:
Publisher:
publish.yml on vtisza/kronikas
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kronikas-0.2.0.tar.gz -
Subject digest:
72ca24cfaef0736f4216473c4b3205f4071fe75d688c715b082657d3d7013b4d - Sigstore transparency entry: 2708272517
- Sigstore integration time:
-
Permalink:
vtisza/kronikas@8044211f6c5fc569420f08eb5f48b4733e7c2603 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/vtisza
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8044211f6c5fc569420f08eb5f48b4733e7c2603 -
Trigger Event:
push
-
Statement type:
File details
Details for the file kronikas-0.2.0-py3-none-any.whl.
File metadata
- Download URL: kronikas-0.2.0-py3-none-any.whl
- Upload date:
- Size: 115.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c0744bca7c5f1c41c94f06e3db734e9ec78c4e53aa1ed2bd48a087edce673b3
|
|
| MD5 |
03b7ab0436cacbff1396da728ea6b368
|
|
| BLAKE2b-256 |
14f9f19b4831fd6778bd00f436c8972b1769164b38dc1c13e064bd57553c19a9
|
Provenance
The following attestation bundles were made for kronikas-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on vtisza/kronikas
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kronikas-0.2.0-py3-none-any.whl -
Subject digest:
2c0744bca7c5f1c41c94f06e3db734e9ec78c4e53aa1ed2bd48a087edce673b3 - Sigstore transparency entry: 2708272568
- Sigstore integration time:
-
Permalink:
vtisza/kronikas@8044211f6c5fc569420f08eb5f48b4733e7c2603 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/vtisza
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8044211f6c5fc569420f08eb5f48b4733e7c2603 -
Trigger Event:
push
-
Statement type: