Skip to main content

cogtraitmodel

DOI tests

Bounded-trait psychometrics in Python: the Anchored Logistic Family (ALF) — the Cognitive Trait Model of Choi (2022) and its generalisation to an arbitrary mastery anchor — with deterministic, quadrature-based estimation.

A trait level of 0.62 means 62% of mastery of a stated task domain, not "0.62 standard deviations above whoever happened to be calibrated".

pip install cogtraitmodel

Why

Item response theory frees the scale from the items but leaves it pinned to the calibration sample: θ = 0 is a statement about other examinees. For formative assessment, learning progressions and adaptive instruction, the question is not who is ahead but how far along is this learner. Placing θ on [0, 1] with 0 and 1 defined by the task domain answers that question directly.

The package makes the model usable inside a response loop:

  • No MCMC. Item parameters come from Bayes modal EM on a Gauss–Legendre grid — 35× faster than a converged random-walk sampler and 73× faster than NUTS, agreeing with both to a θ correlation of 0.9999.
  • A single adaptive update costs about 5 µs, because the grid is fixed on [0, 1] for the lifetime of an item bank and the item-by-node log-probability matrix is precomputed once.
  • Honest uncertainty. Scoring reports the posterior SD, not the Fisher-information SEM, which collapses to zero at the mastery boundary.

Quick start

import numpy as np
import cogtraitmodel as ctm      # the paper's abbreviation, kept short

# simulate
theta = ctm.gen_theta(1000, rng=np.random.default_rng(0))
alpha = np.full(20, 8.0)
beta  = np.linspace(0.1, 0.9, 20)
Y = ctm.gen_responses(theta, alpha, beta, rng=np.random.default_rng(1))

# calibrate items
fit = ctm.bayes_modal_em(Y)

# score persons — MAP point estimate with posterior SD
out = ctm.score(Y, fit["alpha"], fit["beta"])
out["theta"]   # 0.62  ->  "62% of mastery"
out["sd"]      # report this alongside; it decides when to stop testing

Three-parameter (with guessing), information functions, and the empirical dataset used in the paper:

fit3 = ctm.bayes_modal_em(Y, three_p=True)
ctm.tif(np.linspace(0.02, 0.98, 25), alpha, beta)   # test information
Y_lsat = ctm.datasets.build()                       # LSAT Section 6, (1000, 5)

Tracking a learner over time

Bayesian updating alone is not enough when the quantity being measured is changing: carrying the posterior forward asserts "this learner is where they were last time", and it is worse than having no memory at all (RMSE 0.230 against 0.122) while reporting a smaller standard deviation. What is needed is the prediction step of a state-space model — a transition that moves the distribution, not just widens it.

from cogtraitmodel import growth

out = growth.sequential_score(
    responses,      # list of (n, K_t) arrays, one per occasion
    items,          # list of item indices administered at each occasion
    alpha, beta,    # anchored item parameters
    memory="hctm",  # "none" for the memoryless baseline
)
out["theta"]        # (T, n) posterior means
out["sd"]           # (T, n) posterior SDs — these stay honest

The growth curve is the L → ∞ member on the time axis, so no horizon constant is needed. Its four parameters read as entry level, level converged to, rate, and when growth happens; setting delta < gamma expresses forgetting in the same form. Non-monotone trajectories cannot be represented at any parameter setting — there, memoryless scoring is better.

The degrees-of-freedom discipline is enforced, not optional. Releasing parameters as fast as the history grows lets the curve interpolate it exactly, the predictive variance collapses, and the next prior becomes a spike; that condition produced the worst results in the study (RMSE 0.21 with a reported SD seven times too small). fit therefore keeps k ≤ H − 1 and floors the residual variance at the measurement error implied by the posterior SDs.

An arbitrary mastery anchor

Finite L needs no separate estimator, because

P_L(θ; α, β)  ==  P_1(θ/L; αL, β/L)      # exact to machine precision

so fit_L and score_L are rescaling wrappers around the L = 1 path:

fit = ctm.fit_L(Y, L=3.0)                      # item parameters on the L scale
out = ctm.score_L(Y, fit["alpha"], fit["beta"], L=3.0)   # θ ∈ [0, 3]

One consequence is worth stating: the same response data fit equally well at every finite L, so L is a definitional choice rather than a parameter the data identify. Moving the unit is a rescaling; only the origin is structural. L → ∞ is the exception — it does not reduce this way, and it is used as a growth model rather than a measurement model.

The family

One parameter, the mastery anchor L, indexes the family. It says where the mark "1" falls on the ruler; the members share a kernel and differ by a normalising constant.

support anchors fixed reading of θ
L = 1 — CTM [0, 1] origin and unit proportion of mastery
L → ∞ — HCTM [0, ∞) origin only ratio scale, no ceiling

Origin before unit. An origin alone already licenses ratio statements; the unit is a further commitment — that the tasks in hand exhaust the domain. Use L = 1 for measurement, and L → ∞ (cogtraitmodel.hctm) as a growth curve on the time axis, where no finite horizon should be imposed.

What to watch out for

  • Boundary solutions are more dangerous here than in IRT. A perfect scorer gets θ̂ = +∞ under a logistic IRT model, which announces itself; here they get θ̂ = 1, a finite number that reads as "all tasks mastered". Use MAP (the default), which cannot reach the boundary under a Beta prior. score() returns an at_bound flag; MLE is a diagnostic only.
  • Wright maps do not transfer. P(β) = ½ only when β = L/2; otherwise P(β) depends on α.
  • Only α > 0 is representable, so reverse-keyed items cannot be detected. Pre-screen before calibration. On the time axis this makes growth curves monotone: a rise followed by a fall is outside the family.
  • The prior, not the task domain, holds the scale under marginal estimation. Replacing the item domain with its easier or harder half moves θ̂ by under 1.5%; moving the population shifts a fixed examinee by 69% of the score spread. Anchor item parameters when comparing cohorts.

Correctness

p1_naive, p2_naive and p3_naive are literal transcriptions of Eqs. (12)–(14) of Choi (2022) and are kept permanently as the correctness oracle; every optimised path is pinned by tests asserting agreement with them to 1e-12 over α ∈ [0.1, 100]. The bundled LSAT data verifies itself against the published response-pattern frequencies on load.

pytest -q     # 127 tests

Citation

If you use this package, please cite both the software and the model.

@software{choi_cogtraitmodel,
  author  = {Choi, Jaehwa},
  title   = {cogtraitmodel: Bounded-trait psychometrics with the Anchored
             Logistic Family},
  version = {0.1.1},
  year    = {2026},
  doi     = {10.5281/zenodo.22031040},   % concept DOI  resolves to the latest version
  url     = {https://github.com/JaehwaFChoi/ctmpy}
}

@article{choi2022ctm,
  author  = {Choi, Jaehwa},
  title   = {Cognitive Trait Model: Measurement Model for Mastery Level and
             Progression of Learning},
  journal = {Mathematics},
  volume  = {10},
  number  = {15},
  pages   = {2651},
  year    = {2022},
  doi     = {10.3390/math10152651}
}

License

MIT

Release files for cogtraitmodel 0.2.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 cogtraitmodel 0.2.0
File Size Uploaded
cogtraitmodel-0.2.0.tar.gz 28.6 kB Details

Built distribution (wheel)

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

Total release size: 57.8 kB

Release files / cogtraitmodel-0.2.0.tar.gz

Download URL cogtraitmodel-0.2.0.tar.gz
Size 28.6 kB
Tags Source
SHA-256 checksum
How to use checksums
9dd1c7a7d90d45ab547107314d8e7e76459ffbcdae3037585054bf889d6bacae
BLAKE2b-256 checksum
How to use checksums
e03e7672c944096c4e3c906ef20a7e4f9d81f440181fe2987bb90eb48957b841
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 20, 2026.

Transparency log

Release files / cogtraitmodel-0.2.0-py3-none-any.whl

Download URL cogtraitmodel-0.2.0-py3-none-any.whl
Size 29.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
783ccc0aa4db711bb90d465a037b217f076c4122d2b98becce339f1ab0925879
BLAKE2b-256 checksum
How to use checksums
66a5f926beb3957cc132bb55cffec494bfe56ab0952faa79914b7382ddae0caf
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 20, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.2

2 release files

0.2.1

2 release files

This release

0.2.0 This release

2 release files

0.1.1

2 release files

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