factcircuit
FactCircuit는 불변 출처와 단계별 검증, 제한된 검색, 재현 가능한 평가를 제공하는 감사 가능한 주장·증거 검증 루프입니다
수백만 변수까지 확장 가능한 일반화된 다항 probit 모델, 공유 필드 승률·보정·반사실험 제공
A generalization of factor multinomial probit models that scales to millions of variables: shared-field win probabilities, share calibration, counterfactuals.
데이터Python · TeXGitHub @microprediction온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
Scalable econometric algorithms (probit, contests, ratings)
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A package for dealing with races, correlated or not.
Documentation, live demos, and the papers: winning.microprediction.org.
The convergence demo
runs the lattice race against GHK and Mendell–Elston in your browser.README 전문 보기 ↗A package for dealing with races, correlated or not.
Documentation, live demos, and the papers: winning.microprediction.org. The convergence demo runs the lattice race against GHK and Mendell–Elston in your browser.
A race has contestants with abilities and a joint distribution of
performances. race_probabilities prices every contestant's chance of
winning in one pass over a shared field, for Gaussian, Gumbel, or any
other base, and abilities_from_race inverts observed probabilities
back to abilities. Both accept the same covariance descriptions: factor
loadings (V=, D=), a named grammar (structure= independent,
factor, blocks, nested, tree), or a dense cov= that is fitted to the
grammar on the way in.
pip install winning # core depends only on numpy and scipy
winning.certified (optional) returns race win probabilities as Arb
balls that provably contain the answer, for checking the fast engines;
it needs pip install winning[certified] (python-flint) and is never
imported by import winning.
Independent race: shares from abilities, and back.
import numpy as np
from winning import race_probabilities, calibrate_abilities
mu = np.array([-0.5, 0.0, 0.2, 0.3]) # lower is better (min-wins)
p = race_probabilities(mu) # array([0.443, 0.232, 0.175, 0.150])
mu_back = calibrate_abilities(p) # recovers mu (mean zero)
Correlated race: a hundred runners moved by two common factors, all shares in one pass over a shared survival field, then inverted.
rng = np.random.default_rng(0)
N, k = 100, 2
mu = rng.normal(0, 1, N); mu -= mu.mean()
V = rng.normal(0, 0.4, (N, k)) # factor loadings
D = rng.uniform(0.5, 1.5, N) # idiosyncratic variances
p = race_probabilities(mu, V=V, D=D) # all N shares, O(QNL)
mu_hat = calibrate_abilities(p, V=V, D=D) # inversion
Counterfactuals and structure from the same shared field:
from winning import removal_shares, tie_densities
q = removal_shares(mu, V=V, D=D) # q[i][j] = P(j wins | i removed)
w = tie_densities(mu, V=V, D=D) # photo-finish weights: the Jacobian's
# graph-Laplacian (circuit) conductances
The probit conventions. winning.probit takes utilities, with higher
better, and returns shares. It carries both of the paper's
calibrations: utilities from observed shares, and the factor structure
itself from a supplied covariance.
from winning.probit import shares, utilities_from_shares, fit_factor_model
utilities = -mu # higher is better on this side
p = shares(utilities, V=V, D=D) # all N choice probabilities
u = utilities_from_shares(p, V=V, D=D) # the paper's calibration
Sigma = V @ V.T + np.diag(D)
V_hat, D_hat = fit_factor_model(Sigma, k=2) # certified rank-k contrast fit
p2 = shares(utilities, Sigma=Sigma, k=2) # same fit applied en route
Distribution and correlation are chosen per call, and factor probit is one named point in the family.
from winning.factor import race_probabilities
race_probabilities(mu) # the classic independent race
race_probabilities(mu, V=V, D=D) # factor probit
race_probabilities(mu, base="gumbel") # Luce / softmax, exactly
race_probabilities(mu, V=V, base="gumbel") # correlated softmax
race_probabilities(mu, temperature=0.7) # E[softmin(X/tau)]: soft credit
Temperature is exact. By the Gumbel-argmin identity the softmin expectation equals the hard race with each base convolved with the tau-scaled Gumbel kernel, so the same engine serves it. Inversion holds the temperature fixed; a single race pins down the abilities alone.
Custom densities go in as formulas: any standardized survival and
density pair, skewed or multimodal, is a base= callable; give it
cdf (and log_sf) attributes and rare places such as last place and
bottom-k keep full tail precision, as the named bases do. Empirical
data go to winning.classic: histograms, integer scores, and dead
heats with real mass are lattice atoms there, and its multiplicity
calculus prices ties exactly. Formulas to base=, atoms to classic;
the module docstrings give the measured cost of crossing over.
winning.factor: the engine. The all-share forward pass, inversion,
exact Jacobians and tie densities, covariance fitting
(fit_covariance(C, k, blocks=...), where blocks is a count to
cluster or a known label per entrant; fit_tree(C, parent) onto a
known genealogy, priced as race_probabilities(mu, structure=tree)),
and constrained polish.
winning.factor also prices finishing positions: top-k membership at
any depth and factor rank, under the same V, D, F, W the race takes
(top_k_probabilities(mu, k, V, D, F=F, W=W) with the four from
fit_covariance prices a shortlist from a dense covariance), and
rank_probabilities; calibration
of those curves (abilities_from_topk), joint location and scale per
runner from the win and place curves (loc_scale_from_win_and_second),
ordered-prefix probabilities (ordered_probabilities), and the
Plackett-Luce likelihood (winning.factor.permutations).
winning.factor keeps beliefs in the race grammar: exact factor-form
posteriors for common-random-number replicates (crn_posterior, and
crn_posterior_replicates when entrants ran on different replicates)
and for a shared-feature bandit (feature_bandit_posterior), each
returning (m, d, W) ready for race_probabilities(-m, V=-W, D=d).
crn_posterior_replicates also takes, still exactly, a factor or
dense prior (prior=(V0, d0, m0)), per-cell noise (D as a matrix,
or counts= of runs averaged per cell), shared trial and seed shocks
(groups=[(trial_id, U_t), (seed_id, U_s)]) and a nuisance column
level (common="free"). fit_factor_model_missing(Y, k) estimates
the V, D those take from a trials x entrants score matrix with
missing cells (maximum likelihood by EM, with a convergence report).
update_contrast conditions a dense or factor belief on one linear
observation a'theta ~ N(mean, var), such as a judgment of child
minus parent (exact; a factor belief stays exact at one extra
column per two-entrant contrast, or is refitted on request).
winning.probit: the same machine in the probit literature's
max-wins, utilities-and-shares conventions.
winning.classic: the original SIAM-paper lattice ability transform,
in racing vocabulary (dividends, state prices, dead heats).
winning.ratings turns contest results into ratings and predictions
with the race engine underneath, so the model that is fitted is the
model that is priced.
AbilityTracker keeps one belief per entity and updates it from a
finishing order, a winner, scores, or market prices, at a cost
proportional to the entrants of each contest. rate_history and
walk_forward carry a full joint covariance instead and score held-out
predictions. Finishing orders are passed best first, and
order_from_positions, order_from_performance and order_from_times
build one from whatever form the data arrive in.
fit_factor_ratings estimates a level plus offsets over conditions
observed on the contest, such as time control or task category, with
one ridge penalty per covariate: the level pools every contest and the
offsets shrink toward it. On Lichess it beats the site's own
per-time-control Glicko-2 by 0.008 to 0.012 nats per game across three
splits with both sides tuned, and on both Lichess and Chatbot Arena it
beats a single rating and a separate rating per category. sweep_offset_ridge tunes the
penalty, and covariate_contrast_report tells you whether a covariate
was assigned or chosen, which decides how much a null result means.
Block correlation, AbilityTracker(rho=..., groups=...), gives
same-group entrants a shared per-contest shock, fitted and priced under
one model. tune_block_rho selects the share by the filter's own
evidence and can sweep rho and the noise scale together, since the two
trade off under an order likelihood. On Formula 1 the team correlation
is real and changes from season to season, so tune it on a recent
window; the ratings
page has the
measurements.
python -m winning.ratings.verify runs the identities and calibration
checks the engine is held to, from a fast profile in CI to a full
nightly one.
A second ratings line, with whole-density beliefs and exact
full-finish-order updates, is being integrated from src/ and
will ship as ThurstoneRating. Its benchmarks against TrueSkill,
OpenSkill, Glicko-2 and Elo on twelve datasets are in
BENCHMARKS.md: a decisive win on Formula 1 over 1,158
grands prix, the best calibration on chess (ECE 0.0047), statistical
ties at the top on WTA, ATP and the EPL, and markets as the ceiling
wherever they exist.
The manuscripts live under papers/, one folder each.
The correlated calibration is documented in Scalable Share Calibration
for Factor Multinomial Probit Models
(papers/factor-probit-transform): all
shares of a correlated Gaussian race in one O(QNL) pass, matrix-free
graph-Laplacian derivatives, and inversion at ten thousand alternatives
in under a minute. Every number comes from a committed, seeded script in
research/experiments, and
research/experiments/run_all_paper.py regenerates the lot.
research/demos holds explanatory scripts: the shared
survival field, the cavity downdate. js/factor is a
dependency-free JavaScript port checked against the Python (forward
shares to ~1e-15, calibrated abilities to 2e-6), for browser demos. r/winning is a pure-R package.
rust/fastrace holds the optional compiled kernels,
built with pip install maturin && maturin develop --release, and
winning.methods uses them automatically.
julia/winning covers the factor races and the full
top-k module, pinned to the Python reference by Pkg.test and
julia parity/check.jl; the covariance grammar and classic lattice
are next on its roadmap.
Three Julia packages build on it. Two are in the General registry,
] add MultinomialProbit and ] add GMRFExtremes.
julia/MultinomialProbit is the first
multinomial probit for Julia, with exact likelihood, analytic score,
and a common-random-numbers GHK for head-to-heads.
julia/GMRFExtremes answers order-statistic
queries on Gauss-Markov chains, argmax marginals, max CDF, first
passage and expected maximum, and takes a chain from
GaussianMarkovRandomFields.jl through its precision matrix.
julia/FactorMvNormalCDF computes
deterministic MVN rectangle probabilities for factor covariance and
hands every other covariance to MvNormalCDF.jl, which it depends on;
its registration is in progress.
Versions 1.x were the SIAM paper's reference implementation. That API
lives on as winning.classic, maintained, rust-accelerated, and
parity-locked against the R and JavaScript ports, and the old import
paths such as winning.lattice_calibration still work, pointing at the
new home with a DeprecationWarning.
The 2.0 renovation built a density-agnostic engine as a separate
thurstone package. It lives here as winning.research, the retired
research engine: kept for the pipelines built on it, but for
calibration use calibrate_abilities, which is materially faster and
more accurate and uses the compiled kernels. import winning.thurstone
now raises an ImportError with migration guidance, so the external
thurstone shim (which imports through it) fails with the same
message; its consumers should import winning.research directly.
The renovation's
migration notes are preserved in planning/ and
attic/.
For the correlated engine (the shared field, the covariance grammars, the substitution Jacobian, removal counterfactuals, and inversion at scale):
@article{cotton2026inversion,
author = {Cotton, Peter},
title = {Scalable Inversion of Contests with Correlated Performances,
Including Softmax and Multinomial Probit},
year = {2026},
eprint = {2609.01133},
archivePrefix = {arXiv},
primaryClass = {stat.ME},
doi = {10.2139/ssrn.7307363},
note = {arXiv:2609.01133; also SSRN working paper 7307363},
URL = {https://arxiv.org/abs/2609.01133}
}
For the original independent lattice transform (winning.classic):
@article{cotton2021inferring,
author = {Cotton, Peter},
title = {Inferring Relative Ability from Winning Probability in Multientrant Contests},
journal = {SIAM Journal on Financial Mathematics},
volume = {12},
number = {1},
pages = {295-317},
year = {2021},
doi = {10.1137/19M1276261},
URL = {https://doi.org/10.1137/19M1276261}
}
winning.ratings: dynamic and factor ratings on the same engine,
described next.
winning.methods and winning.bench: every rival method behind one
interface, and a seeded accuracy-time benchmark grid,
python -m winning.bench.runner.
winning.research: the density-agnostic research engine for
arbitrary bases.