Skip to main content

ctxchain

Context-dependent Markov transition models for sequence data.

Status: pre-alpha. Everything described here works and is tested. The API is the one the design was written against, not one that grew out of the implementation.

What it is

Estimate transition structure from sequences — customer histories, visit paths, holding-size trajectories — and answer "given where they are now, where are they in N periods?" as a probability distribution, with an interval around it.

>>> import ctxchain as cx
>>> ds = cx.SequenceDataset.from_sequences(
...     [["browse", "cart", "buy"], ["browse", "browse", "cart"], ["cart", "buy"]] * 20
... )
>>> spec = cx.Order(1)
>>> model = cx.fit(cx.accumulate(ds, spec), spec)
>>> model.predict_next(["cart"]).top(2)
[('buy', 0.97...), ('cart', 0.01...)]
>>> round(float(model.forward("browse", steps=3).mean.sum()), 6)
1.0

Every model in the library has one shape:

P(next_state | context(history, covariates))

A first-order chain, a variable-length chain, a time-inhomogeneous chain and a per-segment chain differ only in how context is defined. You declare the context; estimation and prediction are shared.

>>> cx.Order(2)                                    # the previous two states
Order(2, padding='bos')
>>> cx.VariableOrder(max_depth=5).max_depth        # depth chosen per context
5
>>> (cx.Order(1) * cx.TimeIndexed(index="tenure")).state_slot   # state x tenure
0

Why not the usual implementation

Constraint ctxchain
Dense S × S matrices Counts live in a trie over observed contexts; S^k is never allocated
Order limited to ~2 Variable-length contexts, depth chosen per context
Homogeneity baked in Time-varying contexts, with adjacent periods shrunk toward each other
One pooled matrix Grouped contexts that shrink toward the pooled estimate
Censoring ignored Right-censored sequences contribute a context but no final transition
Point estimates only Posterior draws propagated through the N-step forecast

The pipeline

SequenceDataset → accumulate → CountStore → fit → FittedChain

CountStore holds sufficient statistics. Once counting is done the raw data is no longer needed, memory stops depending on how much of it there was, and any number of estimators can be run against the same counts.

>>> counts = cx.accumulate(ds, spec)
>>> counts.n_contexts            # only what was observed, never S**k
2
>>> mle = cx.fit(counts, spec, estimator=cx.estimators.MLE())
>>> backoff = cx.fit(counts, spec, estimator=cx.estimators.Backoff())
>>> mle.n_states == backoff.n_states
True

Getting answers out

The step column may be a date, as long as you say what one step means -- the unit decides what counts as a gap, so it is asked for rather than guessed.

>>> import pandas as pd
>>> frame = pd.DataFrame(
...     {
...         "id": ["u", "u", "u"],
...         "month": pd.to_datetime(["2024-01-31", "2024-02-29", "2024-03-31"]),
...         "plan": ["free", "pro", "pro"],
...     }
... )
>>> monthly = cx.SequenceDataset.from_dataframe(
...     frame, entity="id", step="month", state="plan", step_unit="M"
... )
>>> next(iter(monthly)).steps          # calendar months, not elapsed days
[24288, 24289, 24290]

A fitted model exports a labelled table, answers the long-run question, and survives the session:

>>> plans = cx.SequenceDataset.from_sequences([["free", "pro", "pro"] * 8] * 25)
>>> plan_model = cx.fit(cx.accumulate(plans, spec), spec, estimator=cx.estimators.MLE())
>>> plan_model.to_frame().round(2)     # doctest: +NORMALIZE_WHITESPACE
to     free   pro
from
free   0.00  1.00
pro    0.47  0.53
>>> plan_model.stationary().top(2)     # long-run share of time in each state
[('pro', 0.68...), ('free', 0.31...)]
>>> import tempfile, pathlib
>>> with tempfile.TemporaryDirectory() as tmp:
...     saved = plan_model.save(pathlib.Path(tmp) / "model")
...     cx.FittedChain.load(saved).stationary().top(1)
[('pro', 0.68...)]

stationary() refuses a chain with two closed classes rather than picking one: where such a process settles depends on where it started. With a posterior it solves once per draw, because π is non-linear in P -- the same reason forward never powers the mean matrix.

Estimators

use it for gives you
MLE a baseline, debugging point estimate, no smoothing
DirichletSmoothing small state spaces point estimate, optional posterior
Backoff large sparse spaces (10⁶+ contexts) point estimate, fast, deterministic
HPYP the same hierarchy, with intervals posterior draws, in memory
HierarchicalBayes small spaces, time-varying full posterior via NUTS or SVI

Install

pip install ctxchain

Optional extras: ctxchain[bayes] (NumPyro posterior inference), ctxchain[io] (pandas/parquet readers), ctxchain[viz], ctxchain[sparse].

Tutorials

Both are executable: the test suite runs them.

On uncertainty

Three things this library refuses to do quietly.

Power the mean matrix. E[P]ⁿ ≠ E[Pⁿ]. Posterior draws are propagated one at a time and summarised at the end, so intervals do not collapse as the horizon grows.

Power a matrix that does not describe the chain. A model conditioning on more than one state is not Markov on the states, so there is no S × S matrix to raise to a power at all. forward walks paths that carry their history instead, and transition_matrix asks for the states it is missing rather than quietly returning the start-of-sequence rows.

>>> deep = cx.VariableOrder(max_depth=3)
>>> chain = cx.fit(cx.accumulate(ds, deep), deep)
>>> try:
...     chain.transition_matrix()
... except ValueError as error:
...     print(str(error).split(". ")[0])
VariableOrder(...) conditions on 3 states, so a transition matrix needs the 2 preceding one(s) in history=; got 0
>>> chain.forward(["browse", "cart"], steps=4, n_paths=2000, seed=0).n_paths
2000

Report a number without its support. FittedChain.diagnostics reports per-context coverage, prior dominance and effective order, and the serious warnings appear in the chain's repr whether or not you ask for them.

>>> thin = cx.SequenceDataset.from_sequences([["a", "b", "c"]])
>>> deep = cx.VariableOrder(max_depth=3)
>>> shown = repr(cx.fit(cx.accumulate(thin, deep), deep))
>>> "[error] thin_contexts: 7 of 7 contexts (100%)" in shown
True

Call a what-if a causal effect. replace_transition recomputes a forecast under a modified matrix. Identification is not this library's job, and it does not pretend otherwise — which is why the method is not called intervene.

Not in scope

HMMs and latent-state models, continuous-time chains, deep sequence models, causal-effect estimation, plotting beyond a few inspection helpers. See §2.2 of the design document, and DECISIONS.md for the reasoning behind every choice that was not obvious.

Development

python -m venv .venv && . .venv/bin/activate
pip install -e '.[dev]'
ruff check . && ruff format --check . && mypy && pytest

The suite covers 100% of the package, and pytest --cov fails below 99%. The only coverage exclusions are pragma: no cover on nine environment-dependent or defensive lines, each with its reason written next to it.

Benchmarks are in benchmarks/ and are not run by CI:

python benchmarks/bench_accumulate.py --states 1000 --depth 5 --transitions 10_000_000

License

MIT

Metadata

Release files for ctxchain 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ctxchain 0.1.0
File Size Uploaded Provenance
ctxchain-0.1.0.tar.gz 204.2 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for ctxchain 0.1.0
File Interpreter ABI Platform
ctxchain-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 314.0 kB

Release files / ctxchain-0.1.0.tar.gz

Download URL ctxchain-0.1.0.tar.gz
Size 204.2 kB
Tags Source
SHA-256 checksum
How to use checksums
005e1c168eb62f6d752a6b5958b46960110aebfa24fc2230098bfaf6cb73e1a6
BLAKE2b-256 checksum
How to use checksums
3ee1f1d13be7916f14b81452a8d13858782687cd6b35da493ee90cc088560d69
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 Aug 9, 2026.

Transparency log

Release files / ctxchain-0.1.0-py3-none-any.whl

Download URL ctxchain-0.1.0-py3-none-any.whl
Size 109.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8bcc04d0c3364674df95f88f18ca89dd42587af57adc4324c9320709c8904c82
BLAKE2b-256 checksum
How to use checksums
409490ae3de7ebb2399f047b0d3977d1a6838ff2fb1f3cdf57fa70d3832f26e2
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 Aug 9, 2026.

Transparency log

Security for ctxchain 0.1.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ctxchain-0.1.0.tar.gz

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 Aug 9, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page