FinTFM
A tabular foundation model for corporate credit risk, built from scratch and measured in public.
Given a table of labelled rows, FinTFM predicts new rows in a single forward pass, with your data supplied as context rather than trained on. There are no gradient steps at fit time and no per-dataset tuning — the model is pretrained once, on synthetic data only, and never sees a real table during training.
That last property is the point. A model that provably never saw your benchmark cannot have memorised it, and in a regulated domain the ability to demonstrate that is worth more than a few points of accuracy.
from huggingface_hub import hf_hub_download
from fintfm.inference import FinancialTFMClassifier
ckpt = hf_hub_download("kabartay/fintfm-binary", "v4-cellattn-labels.pt")
clf = FinancialTFMClassifier(ckpt, device="cpu")
clf.fit(X_train, y_train) # stores the table as context; no training happens
pd_estimates = clf.predict_proba(X_test)[:, 1]
The checkpoint is on Hugging Face at kabartay/fintfm-binary — 885K parameters, binary, up to 136 features, Apache-2.0. It is the checkpoint every published binary number below was measured on, so those results are reproducible against this file rather than a variant of it.
Status, stated plainly
FinTFM is a research codebase with a public claims ledger, not a product. It exists to find out whether in-context tabular learning can be made competitive with gradient-boosted trees on financial tables, and to say honestly where it currently is not.
| External benchmark | TabArena, 27 binary datasets against 94 other methods: rank 93 of 95 |
| On real credit panels | calibration consistently among the best measured; discrimination consistently loses to tuned gradient boosting — both, on every panel tried |
| Licence | Apache-2.0, code and weights — chosen separately, not inherited (see Licensing) |
| Checkpoint | kabartay/fintfm-binary — 885K, binary, ≤136 features |
| Tests | 246, plus ruff, the openspec validator, a dependency-licence check and a documentation-link check — all in CI |
| Measurement log | 121 numbered findings, each declaring how its numbers were produced |
| Problem types | binary declared; multiclass and regression implemented but not declared (§121) |
Read docs/paper/CLAIMS.md before anything else. Every claim this
project could make is tagged SURVIVES / SINGLE DRAW / SUPERSEDED / RETRACTED / OPEN against its
evidence. That file, not this one, is the source of truth — and it records what stopped being
true as prominently as what holds.
Throughout this repository, §N refers to entry N in
docs/results/FINDINGS.md, a measurement log in which every number states how it was produced (MEASURED, SMOKE-TEST, SIMULATED or ESTIMATED) and negative results are kept deliberately, because they are what stops the same wrong conclusion being reached twice.
Results, as measured
TabArena, 27 binary datasets, one fold each, against 94 other methods:
| change | mean ROC-AUC | Elo | rank |
|---|---|---|---|
| label encoding (§98) | 0.7642 | 662 | 93 / 95 |
| + out-of-fold target statistics (§101) | 0.7823 | 765 | 93 / 95 |
| + multiclass-capable prior (§105) | 0.7817 | 813 | 93 / 95 |
| + 5.0M parameters, matched task count (§114) | 0.7774 | 751 | 93 / 95 |
| + tree-structured prior (§115), two seeds | 0.7926 / 0.7913 | 842 / 826 | 93 / 95 |
One change has moved the number on the benchmark that matters, and it was preprocessing rather than architecture. The tree prior adds a further +0.0094 here and replicates across two seeds — but costs −0.0221 average precision on credit data (§116), negative on 5 of 5 folds with three surviving multiple-comparison correction at p < 0.001, so it ships off by default. Everything else tried — parameter scale at matched task volume, training volume, a widened structural-causal prior — has been null or negative. The rank has never moved.
What this project does that others do not
Three things, stated at their true weight:
Incoherent PD term structures are impossible by construction. The field's standard per-horizon construction produces non-monotone cumulative default curves for 39% of firms on real data — a firm whose 3-year default probability is below its 2-year. A hazard head makes that unrepresentable, at zero measured accuracy cost.
A severe capacity defect was found by measurement, not review. The original architecture had capped discrimination regardless of true task difficulty, caught against an exactly-known Bayes-optimal AUC. Its cause was isolated to the architecture rather than the prior after eliminating seven other candidates one at a time, then closed by a two-way cell-attention change worth +0.042 average precision on real data.
The record corrects itself in public. This README has been wrong about its own results at least twice (§107, §112). Both are retracted in place, with the reasoning kept. No peer project publishes a document that tags its own claims RETRACTED as prominently as its wins, and in a domain where a model must arrive with its own validation evidence, that is the differentiator.
How it works
- Priors (
src/fintfm/prior/) — synthetic task generators, sampled and mixed per batch (mixture.py).financial.py— a structural generative story for company balance sheets, P&L and default labels: accounting identities, sector and macro effects, a wide derived-ratio family, missingness, a sharpness (signal-to-noise) knob, and a default-rate envelope that can reach real low-default-portfolio rates (~0.2%).scm.py— a generic random-graph structural-causal-model prior (the TabPFN/TabICL-style idea): random layered functions with several nonlinearities, for general nonlinear and multiclass structure the financial prior does not cover.tree.py— ensembles of oblivious decision trees, generating the axis-aligned, piecewise-constant structure real tabular data is full of and the other priors do not. Added on distinctiveness grounds afterfintfm-priorscoremeasured it as the only member of the mixture with a positive tree-versus-linear gap (§112). Off by default (p_tree=0.0).trivial.py,crossed.py— diagnostic-only priors used to isolate specific hypotheses (whether the architecture can learn at all; whether a prior's features or its label mechanism carries a measured effect). Not part of the default training mixture.
- Model (
src/fintfm/modeling/model.py) — a from-scratch Transformer. Cells are embedded individually and carry a random per-task column identity (so the model can tell columns apart without a positional embedding, which would break invariance to column order). Columns attend to each other within a row; optionally —ModelConfig.n_cell_blocks, currently experimental — cells also attend across rows within one feature before that, giving each column a distribution-derived identity rather than only a random tag. Rows are then pooled and attend to context rows to perform in-context learning. An optional hazard head (modeling/hazard.py) produces a cumulative PD term structure that is monotone by construction. Independent implementation of ideas described in the public TabPFN / TabICL literature — no code or weights from any existing project (see Licensing below). - Training (
src/fintfm/modeling/train.py,fintfm-train) — infinite synthetic data, one gradient step per fresh batch, cosine LR schedule, checkpointing, a held-out quality metric that reports per-task AUC (not the pooled-across-tasks number, which is inflated whenever task base rates differ) and Brier skill against a base-rate-only predictor. - Inference (
src/fintfm/inference/) — an sklearn-compatibleFinancialTFMClassifier:fit()stores the table as context,predict_proba()runs the frozen network. Several context-construction strategies (uniform, balanced, hybrid, retrieval, prototype) and a base-rate correction, plus ensembling over column-identity draws to recover the distributional column-order invariance a random identity trades for expressiveness.inference/categorical.pyencodes categorical columns as out-of-fold smoothed target statistics, because the model reads every cell as an ordered scalar and label encoding's arbitrary order is measurably worse than no order at all (§100). The out-of-fold step is not a refinement: the naive form puts a row's own label into its own encoding, which makes the context self-predictive and the feature absent at query time — it harms the model rather than flattering the score, so it survives careless validation.inference/regressor.pyadds aFinancialTFMRegressoron the same frozen network and no new architecture: a continuous target cut into K quantile bins is an integer index over K outcomes, so the existing classification head regresses as-is, and the output is natively distributional — quantiles and prediction intervals come free, and the predicted density can be bimodal, which is what loss given default actually is and what a Gaussian head cannot represent. - Evaluation (
src/fintfm/evaluation/,src/fintfm/experiments/) — real corporate-default panels (V4FinBench via its published protocol,fintfm-v4protocol; Polish and Taiwan bankruptcy,fintfm-bench), an out-of-time harness, a synthetic capability suite (fintfm-capability, including a class-count sweep for multiclass, §99) with probes of a known ceiling — some with a closed-form Bayes-optimal AUC — specifically built to catch a model that looks fine on average while capped in a way an aggregate score cannot see. Average precision is reported alongside ROC-AUC everywhere, and read first at low base rates (ROC-AUC's chance floor is 0.5 regardless of prevalence; AP's floor is the prevalence itself, so it stays legible at the base rates this project's target segment actually has).fintfm-priorscorescores a prior rather than a model, on the three criteria the literature converged on — performance, diversity, distinctiveness — using fitted baselines only, so it cannot confuse "the prior lacks this structure" with "our model cannot learn it". Its first version could, and §112 is the record of what that cost.
Quickstart
uv sync --extra bench --extra hf # naming one extra uninstalls the others
uv run pytest # 246 tests; 1 skip is expected, more means look
Using a checkpoint
The estimators are scikit-learn compatible. fit() stores the table as context — it takes
no gradient steps — and predict_proba() runs the frozen network once.
from fintfm.inference import FinancialTFMClassifier, FinancialTFMRegressor
clf = FinancialTFMClassifier(ckpt, device="mps") # ckpt from hf_hub_download, above
clf.fit(X_train, y_train) # stores context; no training happens
proba = clf.predict_proba(X_test)[:, 1]
reg = FinancialTFMRegressor("runs/v4-regression.pt", n_bins=10)
reg.fit(X_train, y_cont)
point = reg.predict(X_test) # distribution mean
lo, hi = reg.predict_interval(X_test, 0.8) # 80% interval, free from the same head
Categorical columns need encoding before they reach the model — it reads every cell as an
ordered scalar, and label encoding is measurably worse than no order at all (§100). Use
fintfm.inference.categorical.CategoricalTargetEncoder, which is out-of-fold on the context
rows for reasons that are not optional; see How it works.
Training a checkpoint
uv run fintfm-train --steps 300 --d-model 32 --d-cell 16 --n-layers 2 --max-features 16 \
--max-classes 2 --device cpu --out runs/v0-smoke.pt # pipeline check, a couple of minutes
uv run fintfm-bench --model runs/v0-smoke.pt --credit # real corporate-default panels
That smoke config exists to check the pipeline runs, not to produce a usable checkpoint — see
docs/infra/COMPUTE.md for measured step costs at real scale. A real pretraining
run (thousands of steps, --d-model 128+, --max-features matching your data) takes
hours-to-a-day and needs a GPU: --device mps on Apple Silicon, --device cuda, or see
docs/infra/HF_JOBS.md for the Hugging Face Jobs recipe this project actually
uses for training runs. Check uptime before running anything heavy locally — see
CLAUDE.md for why.
Both fintfm-bench --credit (Polish/Taiwan need 64/95 features) and fintfm-v4protocol
(V4FinBench needs 136) will print SKIPPED for fintfm: model takes 16 features, data has N —
correct, expected behaviour for the smoke checkpoint above, not a bug. Baselines still run and
score normally; only the FinTFM arm needs a checkpoint pretrained with a matching
--max-features to be evaluated.
Configuration
The numbers experiments use — split years, context sizes, strategies, seeds, scoring
thresholds, the prior's default-rate and sharpness envelope — live in
src/fintfm/configs/default.yaml, not scattered through
the code. Every entry point takes --config with a file that is deep-merged over that
default, so an override carries only what it changes:
uv run fintfm-ctxsweep --model runs/m.pt --config configs/context-sweep-3seed.yaml
uv run fintfm-v4oot --model runs/m.pt --config configs/retrieval-best.yaml
Resolution order is packaged default → --config (or $FINTFM_CONFIG) → explicit CLI flag.
Runs print the layers they used and record them as config_sources in their output JSON.
Unknown keys are refused by name rather than ignored, and an overlapping train/test split is
rejected before the run starts. configs/README.md explains what
belongs in configuration and what deliberately stays in code; several files under
configs/ are deliberately diagnostic, not production (their own headers say
so), used to isolate a single variable while chasing a specific finding.
Licensing / provenance
Everything here — the prior, the architecture, the training loop — is original code written
for this project. No weights, datasets, or source from Neuralk (Seldon), Fundamental
(NEXUS), Google TabFM, TabPFN, TabICL, TabDPT, or any other tabular-foundation-model product
are used or may be added. Those are cited in project discussion purely as public
research/product context. Every third-party dataset or dependency's licence is checked before
use, and a weight licence is checked separately from its code licence, every time — never
assumed from a prior check. Direct dependencies are all permissive — numpy, pandas and
scikit-learn BSD-3, scipy BSD-3, torch BSD-3, PyYAML MIT, and the optional benchmark extras
lightgbm MIT, xgboost Apache-2.0, catboost Apache-2.0, pyarrow Apache-2.0 — and
scripts/check_licences.py enforces that in CI rather than leaving it to this paragraph.
One honest exception, which CI found and this paragraph previously did not mention. On
Linux, torch pulls in around fifteen NVIDIA CUDA runtime packages that are NVIDIA
Proprietary, not permissive. They are a transitive runtime dependency rather than a choice
made here, this project does not redistribute them — uv build produces a pure-Python
wheel and pip fetches them from PyPI under NVIDIA's own terms — and they are absent on macOS,
which is why a check run only on a developer's Mac reported everything permissive while CI did
not. Anyone shipping this in a product must read NVIDIA's EULA themselves; it is not
something this repository can assert on their behalf.
What would change the picture
Stated so the project is falsifiable rather than open-ended. The ~0.035 deficit to the field has no measured axis of variation, and these are the remaining candidates:
| still open | why it is a candidate | status |
|---|---|---|
| factorized attention | the encoder is memory-bound at every turn, and three peers independently chose the cheaper form | proposed (44.x), prior art recorded |
| a joint objective | p(x, y | D) rather than p(y | x, D) makes every column a training signal, not just the target |
proposed (48.15), scoped as a measurement before a rewrite |
| prior design | the only lever a peer's own ablations identify as decisive | instrument built and partly falsified — see below |
Closed by measurement
Not abandoned — tested and ruled out, which is the more useful half of the record and the reason the list above is short:
| candidate | what the measurement said |
|---|---|
| parameter scale | −0.0049 at 5.7× the parameters on matched task volume (§114). A peer's published curve returns +0.0049 R² for 16.7×, against a 0.035 deficit. |
| training volume | null at 5× (§93); +0.0028 at 2× (§114). |
| depth over width | could not be scored at all — the harness's per-dataset time limit, after 8 of 27 datasets (§114). Depth costs inference time and this model is already slow. |
column_id_dim |
peaks at the value chosen by accident; all four non-peak arms below it on all five folds (§104). |
| a widened structural-causal prior | p = 1.000 against its own matched control. |
| a tree-structured prior | +0.0094 on general tabular data, replicated across two seeds (§115) — and −0.0221 average precision on credit (§116). Ships off by default. |
The prior-scoring instrument is half-confirmed and half-falsified. It predicted the general-tabular gain above before it was measured (§112 → §115). A second arm, added to ask the credit question, then predicted the wrong direction on its first real test — raising the structural-causal prior's share cost −0.0339 AP, 0 of 5 folds, all five significant (§118). So it predicts breadth, not fit, and no claim rests on its credit arm.
Reproducing the measurements
Every number in docs/results/FINDINGS.md names the command that produced it.
The entry points:
| command | what it measures |
|---|---|
fintfm-v4protocol |
V4FinBench under its published five-fold protocol — the benchmark this project's claims rest on |
fintfm-bench --credit |
Polish and Taiwan bankruptcy panels |
fintfm-v4oot |
out-of-time split, which the published protocol is not |
fintfm-capability |
synthetic probes with a known ceiling, some with a closed-form Bayes-optimal AUC |
fintfm-priorscore |
scores a prior, not a model, on performance / diversity / distinctiveness |
fintfm-ctxsweep |
context construction, which explains more variance than model family (§5) |
Two conventions worth knowing before reading any of it. Every number declares how it was produced — SMOKE-TEST, MEASURED, SIMULATED or ESTIMATED — because in an ML repository a wrong number does not crash, it looks like a result. And negative and superseded results are kept, because they are what stops the same wrong conclusion being reached twice.
Where this is going
docs/ holds the project's reasoning, indexed in docs/README.md.
docs/paper/CLAIMS.md— start here. Every claim this project could make, tagged by status, newest evidence wins.docs/results/FINDINGS.md— the full measurement log, numbered sequentially (121 entries and counting), each declaring how its numbers were produced.docs/design/DECISIONS.md— why the project is built the way it is, and what would reverse each choice.docs/roadmap/STRATEGY.md— the plan of record.docs/design/ARCHITECTURE.md— how the model works, in more depth than this file.docs/results/TABARENA.md— the external-evaluation recipe, the coverage fraction any score must carry, and four silent failure modes including a results cache that returns stale numbers after a preprocessing change.docs/paper/RELATED_WORK.md— the competing models, read from primary sources, with what each costs this project's positioning. Its digest is the shortest useful summary of why the remaining lever is the prior.docs/results/POSTMORTEM.md— wrong diagnoses, each caught by measurement rather than review, kept on the record deliberately.openspec/changes/— the live roadmap as structured proposals with verifiable tasks, not prose.uv run python openspec/tools/validate.pychecks every proposal is well-formed and every finding declares its provenance.
Short version: the mechanism claims ("no training on your data", "no feature engineering") are already owned by better-funded competitors, so the thesis is not the mechanism. It is a credit model that arrives with its own validation evidence — calibrated, auditably free of benchmark contamination, and eventually backed by a pre-registered forward track record that cannot be bought — plus a public, self-correcting record of what has and has not been shown to be true.
Citing
A CITATION.cff is in the repository root, so GitHub's "Cite this repository" button
renders BibTeX and APA directly.
Cite the repository for the method, the measurement log or any finding; cite the checkpoint (kabartay/fintfm-binary) when the specific weights matter to what you are reporting. They are different artifacts and a reader can only check the one you name.
Contributing, and what cannot come in
Issues and discussion are welcome. Two hard boundaries, both of which protect the only claim this project has that competitors do not:
No code, weights, or training data from any tabular-foundation-model product — TabPFN,
TabICL, TabDPT, LimiX, Nori, MITRA, or any other — may enter this repository. Their published
papers are legitimate to read and cite, and docs/paper/RELATED_WORK.md
does exactly that for ten of them. Ingesting any of it would destroy the provenance argument
above, and nothing would warn us.
Check a weights licence separately from its code licence, every time. Four of the ten peer projects read for this release ship permissive code with non-commercial weights, a split invisible from a repository's headline licence badge. One of them restricts commercial use of the model's output, not merely the weights.
Release files for fintfm 0.5.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fintfm-0.5.5.tar.gz | 171.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fintfm-0.5.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 366.3 kB
Release files / fintfm-0.5.5.tar.gz
| Download URL | fintfm-0.5.5.tar.gz |
|---|---|
| Size | 171.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e8a3e326173fcf32eeb1972b421dcf7f01ec247b4e5414620aa403aba9b4d304
|
|
BLAKE2b-256 checksum How to use checksums |
251fd6378410b62b48c5a60b528117ce3d97c4c99eb7242fa64a41f55644de21
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / fintfm-0.5.5-py3-none-any.whl
| Download URL | fintfm-0.5.5-py3-none-any.whl |
|---|---|
| Size | 195.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f6832076f7e2b7e34f4cc3ad112925ec533a48a581cc20adea43052bac1519d3
|
|
BLAKE2b-256 checksum How to use checksums |
c6512e10182248bce29a4c7f1e5f6344c6b152eacd305b96dfbd77b6303418fb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log