Skip to main content

RaschPy

RaschPy is a Python package for Rasch analysis which can estimate parameters for a variety of Rasch models, generate a range of model fit statistics, and output tables and graphical plots. RaschPy also contains simulation functionality. RaschPy is open source and free to download. The following is intended to highlight the main functionality of RaschPy rather than serve as a comprehensive user manual; a full navigable manual is available in the GitHub repository. A basic Excel spreadsheet demonstrating the PAIR algorithm for dichotomous data is also available, following the example of the Moulton JMLE dichotomous demo available via https://www.rasch.org/moulton.htm (and using the same set of responses — the final results are compared to the Moulton JMLE output).

Models

RaschPy has a parent class Rasch for analysis, with the following child classes for different Rasch models:

  • SLM for the simple logistic model (dichotomous Rasch model) (Rasch 1960)
  • PCM for the partial credit model (Masters 1982)
  • RSM for the rating scale model (Andrich 1978)
  • MFRM for the many-facet Rasch model (rating scale model formulation) (Linacre 1994), including extended rater representations (Elliott and Buttery 2022a, Elliott 2025), with five rater parameterisations (global, items, thresholds, bivector and matrix formulations)

Analysis

To analyse data, create an object in the appropriate class, passing a pandas DataFrame of response data as an argument along with other arguments relevant to the chosen Rasch model, such as the maximum score for RSM or MFRM, or a vector of maximum scores for PCM. At the time of writing, the RSM and MFRM classes only support a single response group (i.e. all items must have the same threshold structure), and the MFRM class only supports one additional facet for rater severity. Parameter estimation uses variants of PAIR (Choppin 1968, 1985), the eigenvector method (Garner & Engelhard 2002, 2009) and CPAT (Elliott & Buttery 2022a, 2022b).

Each model follows the same workflow: instantiate → calibrate → fit statistics → output tables → plots. All major results are stored as attributes on the model object after each step.

Examples

Loading data

RaschPy includes loaders for CSV, Excel, and JSON that validate scores and handle missing data:

from raschpy.loaders import loadup_slm, loadup_pcm, loadup_rsm
from raschpy.loaders import loadup_mfrm_single, loadup_mfrm_xlsx_tabs, loadup_mfrm_multiple

# Wide-format file (persons as rows, items as columns)
responses, invalid = loadup_slm('my_data.csv')
responses, invalid = loadup_rsm('my_data.csv', max_score=4)
responses, invalid = loadup_pcm('my_data.csv', max_score_vector=[3, 3, 4, 4, 3])

# Long-format file (Person, Item, Score columns)
responses, invalid = loadup_rsm('my_data.csv', max_score=4, long=True)

# MFRM: one file with (Rater, Person) MultiIndex
responses, invalid = loadup_mfrm_single('my_mfrm_data.csv', max_score=3)

# MFRM: one Excel workbook with one sheet per rater
responses, invalid = loadup_mfrm_xlsx_tabs('my_mfrm_data.xlsx', max_score=3)

# MFRM: separate files per rater
responses, invalid = loadup_mfrm_multiple(
    {'Rater_A': 'rater_a.csv', 'Rater_B': 'rater_b.csv'}, max_score=3
)

Alternatively, pass any pandas DataFrame directly to the model constructor.


Simple Logistic Model (SLM)

Dichotomous data (0/1). Each row is a person, each column an item.

from raschpy import SLM
from raschpy.simulation import SLM_Sim

# Pass a simulation object directly — generating parameters are attached to m.generating
sim = SLM_Sim(no_of_items=10, no_of_persons=300)
slm = SLM(sim)

# Or pass a DataFrame as usual
slm = SLM(responses)
slm.calibrate()          # PAIR item location estimation
slm.fit_statistics()     # item, person, and test-level fit

slm.item_stats_df(full=True)
print(slm.item_stats)    # Estimate, SE, Infit MS, Outfit MS, ...

slm.person_stats_df()
print(slm.person_stats)  # Estimate, CSEM, Score, Infit MS, ...

slm.test_stats_df()
print(slm.test_stats)    # ISI, PSI, reliability

slm.icc(item='Item_1', obs=True)     # Item Characteristic Curve
slm.tcc(obs=True)                    # Test Characteristic Curve
slm.test_info()                      # Test Information Curve
slm.std_residuals_plot(normal=True)  # Standardised residuals histogram

slm.save_stats('slm_results', format='xlsx')

Score lookup and person estimation

# Convert raw scores to person location estimates
model.score_lookup_table()
print(model.score_lookup)   # pandas Series indexed by raw score

# By default, missing responses are excluded from person estimates.
# To include them, treated as incorrect rather than missing:
model.person_estimates(missing_as_incorrect=True)

Partial Credit Model (PCM)

Polytomous data where items may have different maximum scores.

from raschpy import PCM
from raschpy.simulation import PCM_Sim

# Pass a simulation object directly — generating parameters are attached to m.generating
sim = PCM_Sim(no_of_items=5, no_of_persons=300, max_score_vector=[3, 3, 4, 4, 3])
pcm = PCM(sim)

# Or pass a DataFrame as usual
pcm = PCM(responses, max_score_vector=[3, 3, 4, 4, 3])
pcm.calibrate()
pcm.fit_statistics()

pcm.item_stats_df(full=True)
print(pcm.item_stats)                  # Central item locations

pcm.threshold_stats_df(full=True)
print(pcm.threshold_stats_uncentred)   # Uncentred threshold estimates
print(pcm.threshold_stats_centred)     # Centred threshold offsets

pcm.icc(item='Item_1', obs=True)             # Expected score curve
pcm.crcs(item='Item_1', obs='all')           # Category Response Curves
pcm.threshold_ccs(item='Item_1', obs='all')  # Threshold Characteristic Curves
pcm.tcc(obs=True)

pcm.score_lookup_table()
print(pcm.score_lookup)   # pandas Series indexed by raw score

pcm.save_stats('pcm_results', format='xlsx')

Rating Scale Model (RSM)

Polytomous data where all items share the same rating scale structure.

from raschpy import RSM
from raschpy.simulation import RSM_Sim

# Pass a simulation object directly — generating parameters are attached to m.generating
sim = RSM_Sim(no_of_items=8, no_of_persons=300, max_score=4)
rsm = RSM(sim)

# Or pass a DataFrame as usual
rsm = RSM(responses, max_score=4)
rsm.calibrate()
rsm.fit_statistics()

rsm.item_stats_df(full=True)
print(rsm.item_stats)      # Item locations

rsm.threshold_stats_df(full=True)
print(rsm.threshold_stats) # Shared Rasch-Andrich thresholds

rsm.icc(item='Item_1', obs=True)
rsm.crcs(obs='all')         # Pooled across all items
rsm.threshold_ccs(obs='all')
rsm.tcc(obs=True)

rsm.score_lookup_table()
print(rsm.score_lookup)   # pandas Series indexed by raw score

rsm.save_stats('rsm_results', format='xlsx')

Many-Facet Rasch Model (MFRM)

Polytomous data with multiple raters. Data must be a DataFrame with a (Rater, Person) MultiIndex and items as columns. Five rater parameterisations are available, selected at calibration time:

model= Rater severity structure
'global' Single scalar severity per rater
'items' Separate severity per rater × item
'thresholds' Separate severity per rater × threshold
'bivector' Separate severities per rater × item and per rater × threshold
'matrix' Full severity matrix per rater × item × threshold
from raschpy import MFRM

mfrm = MFRM(responses)
mfrm.calibrate(model='global')
mfrm.fit_statistics(model='global')

mfrm.item_stats_df(model='global', full=True)
print(mfrm.item_stats_global)        # Item locations

mfrm.threshold_stats_df(model='global', full=True)
print(mfrm.threshold_stats_global)   # Shared thresholds

mfrm.rater_stats_df(model='global', full=True)
print(mfrm.rater_stats_global)       # Rater severities and fit

mfrm.person_stats_df(model='global')
mfrm.test_stats_df(model='global')

mfrm.icc(item='Item_1', model='global', obs=True)
mfrm.crcs(item='Item_1', model='global')
mfrm.tcc(model='global', obs=True)

mfrm.save_stats(model='global', filename='mfrm_results', format='xlsx')

The same object can hold calibrations for multiple parameterisations simultaneously:

mfrm.calibrate(model='items')
mfrm.fit_statistics(model='items')
mfrm.rater_stats_df(model='items', full=True)
print(mfrm.rater_stats_items)  # Per-item severity table

Extreme and invalid persons

Persons with entirely missing data are always removed at instantiation and stored in m.invalid_responses. Persons with extreme scores (all-zero or perfect) are removed only when extreme_persons=False is passed to the constructor, and are stored in m.extreme_persons.


Many-Facet Rasch Model — Bivector formulation

The bivector formulation represents rater severity as an additive combination of per-item leniency effects and per-threshold consistency effects, allowing rater behaviour to vary systematically across the latent continuum. It functions as rater-as-RSM, as opposed to the matrix formulation, which functions as rater-as-PCM

mfrm.calibrate(model='bivector')
mfrm.fit_statistics(model='bivector')

mfrm.rater_stats_df(model='bivector', full=True)
# rater_stats_bivector has MultiIndex columns:
# per-item marginal severities, then per-threshold marginal severities,
# plus overall fit statistics
print(mfrm.rater_stats_bivector)

mfrm.icc(item='Item_1', model='bivector', obs=True)
mfrm.tcc(model='bivector', obs=True)

mfrm.save_stats(model='bivector', filename='mfrm_bivector_results', format='xlsx')

Model comparison — RSM vs PCM threshold structure

RSM constrains every item to share the same threshold structure; PCM lets each item have its own. Since RSM is nested within PCM, model_selection() compares them directly via a likelihood-ratio test, AIC, or BIC (requires uniform max scores across items):

rsm.model_selection(test='AIC')
print(rsm.model_comparison_rsm_pcm_aic_summary)    # ranked comparison table
print(rsm.model_comparison_rsm_pcm_aic_preferred)  # 'RSM' or 'PCM'

# Equivalently from the PCM side
pcm.model_selection(test='LR')
print(pcm.model_comparison_rsm_pcm_lr_summary)

MFRM rater-parameterisation model selection

model_selection() calibrates all five rater parameterisations (global, items, thresholds, bivector, matrix) and ranks them by the chosen criterion:

mfrm.model_selection(test='AIC')
print(mfrm.model_comparison_mfrm_aic_summary)    # ranked comparison across all five models
print(mfrm.model_comparison_mfrm_aic_preferred)  # e.g. 'items'

Mixed model: per-rater parameterisation assignment

Rather than forcing every rater into the same parameterisation, per_rater_model_selection() assigns each rater the simplest adequate structure individually, top-down (matrix → bivector → items/thresholds → global):

mfrm.per_rater_model_selection(test='AIC')
print(mfrm.rater_models)                      # Series: rater -> assigned parameterisation
print(mfrm.per_rater_model_selection_table)   # full per-rater testing-ladder results
print(mfrm.per_rater_model_selection_counts)  # how many raters landed on each parameterisation

mfrm.fit_statistics(model='mixed')            # fit statistics using each rater's assigned model
mfrm.rater_stats_df(model='mixed', full=True)
print(mfrm.rater_stats_mixed)

Category width statistics

For PCM, RSM, and MFRM, category_stats_df() reports each item's category widths (the gap between consecutive thresholds) rather than raw threshold locations — a width below 0 flags local category disordering:

pcm.category_stats_df()
print(pcm.category_stats)   # Estimate (width), SE, Disordered, Prop disordered

mfrm.category_stats_df(model='global')
print(mfrm.category_stats_global)

Differential Item Functioning (DIF) and invariance testing

dif_test() splits persons by an exogenous covariate (e.g. Gender, L1), calibrates each group independently, purifies them onto a common scale (so genuine DIF items can't distort the scale used to detect DIF), and tests every item for DIF against a chosen reference group — supporting covariates with any number of levels, each tested individually against the reference:

import pandas as pd
from raschpy import SLM

exogenous = pd.DataFrame({'Gender': [...]}, index=responses.index)  # one row per person
slm = SLM(responses, exogenous=exogenous)

slm.dif_test(covariate='Gender')
print(slm.dif_table)          # per-item, per-focal-group DIF statistics
print(slm.dif_omnibus_table)  # Andersen-style joint test per focal group

For a simpler two-group check (a single Andersen likelihood-ratio test rather than the full anchor-purification workflow), use andersen_lr_test(split_by='exogenous') — note split_by='person_location'/'score' is disabled as a general model-fit test (found to have no power in simulation), so this is exogenous-covariate-only:

slm.andersen_lr_test(split_by='exogenous', covariate='Gender')
print(slm.andersen_lr, slm.andersen_df, slm.andersen_p)

dif_test() is available on SLM, PCM, and RSM — it is not yet implemented for MFRM. andersen_lr_test() is available on all four, with per-parameterisation variants on MFRM (e.g. andersen_lr_test_global).


Anchor calibration to an external item bank

To place a new calibration on the same scale as a previously-calibrated item bank, pass a dict or pandas.Series of externally-supplied item locations, keyed by item name. A subset of "anchor" items in common with the bank is used to compute a translation constant, with outlier anchors automatically down-weighted or excluded:

bank_locations = {'Item_1': -0.8, 'Item_2': 0.3, 'Item_3': 1.1}

slm.calibrate_anchor(bank_locations)
print(slm.anchor_items)      # item locations shifted onto the bank scale
print(slm.anchor_summary)    # anchors supplied/selected/dropped, correlation, SD ratio, translation constant
print(slm.anchor_selection)  # per-anchor-item diagnostics (which were kept/dropped as outliers)

Available identically on SLM, RSM, and PCM. Diagnostics are plotted automatically (plot=True by default) via plot_anchor_selection(), which can also be called manually against any anchor_selection-shaped table.


Simulation

RaschPy includes simulation classes for generating synthetic data under each model. Simulation runs automatically on instantiation; generating parameters are stored as attributes on the simulation object (sim.items, sim.persons, sim.thresholds, and for MFRM models sim.facet_effects). When a simulation object is passed directly to a model constructor, the generating parameters are also attached to the model object under a generating namespace, making recovery comparisons straightforward:

from raschpy.simulation import SLM_Sim, RSM_Sim, PCM_Sim
from raschpy.simulation import MFRM_Sim_Global, MFRM_Sim_Items, MFRM_Sim_Thresholds, MFRM_Sim_Bivector, MFRM_Sim_Matrix

sim = SLM_Sim(no_of_items=10, no_of_persons=300, item_range=3, person_sd=1.5)
data = sim.responses   # pandas DataFrame

sim = RSM_Sim(no_of_items=8, no_of_persons=300, max_score=4)
data = sim.responses   # pandas DataFrame

sim = PCM_Sim(no_of_items=6, no_of_persons=300, max_score_vector=[3, 3, 3, 4, 4, 4])
data = sim.responses   # pandas DataFrame

sim = MFRM_Sim_Global(no_of_items=6, no_of_persons=200, no_of_raters=4, max_score=3)
data = sim.responses   # (Rater, Person) MultiIndex DataFrame

# Pass the sim object directly to the model constructor to attach generating parameters
from raschpy import MFRM
mfrm = MFRM(sim)
mfrm.calibrate(model='global')

# Compare generating and estimated parameters
print(sim.items)                      # Generating item locations
print(mfrm.generating.items)          # Same, accessible on the model object
print(mfrm.items)                     # Estimated item locations

# Also for rater severities etc,
print(sim.facet_effects)              # Generating rater severities
print(mfrm.generating.facet_effects)  # Same, accessible on the model object
print(mfrm.raters_global)             # Estimated rater locations

Bootstrap standard errors and confidence intervals

Standard errors are computed by bootstrap resampling. They are triggered automatically inside fit_statistics(), or can be run explicitly to request confidence intervals:

model.std_errors(no_of_samples=200, interval=0.95)
model.item_stats_df(interval=0.95)  # adds 2.5% and 97.5% columns

Anchor calibration — MFRM raters

To place an MFRM estimate within an anchored frame of reference, relative to the mean of a set of 'gold standard' raters, pass a list of anchor raters. A new set of anchored estimates will be generated. (For anchoring SLM/RSM/PCM item locations onto an external item bank instead, see Anchor calibration to an external item bank above.)

mfrm.calibrate_global()
print(mfrm.raters_global)                    # Unanchored rater severities

anchors = ['Rater_1', 'Rater_3', 'Rater_6']
mfrm.calibrate_global_anchor(anchors)
print(mfrm.anchor_raters_global)             # Anchored rater severities

Checking anchor rater homogeneity

Anchoring only constrains the mean severity of the chosen anchor raters to zero — it has no way to notice whether that mean is a stable, shared reference point or an artefact of averaging over raters who don't behave alike. check_anchor_homogeneity() is a separate, opt-in diagnostic that tests whether the proposed anchor set actually agrees with itself before you rely on it:

mfrm.check_anchor_homogeneity(model='global', anchors=anchors)
print(mfrm.anchor_homogeneity_test)        # omnibus Cochran's Q test
print(mfrm.anchor_homogeneity_per_rater)   # per-rater severity, SE, z, p, Flagged

Usage and citation

RaschPy is provided as freeware under an Apache 2.0 Licence (see LICENSE file in this repository for details). Users are free to use or modify the code for their own purposes, but should cite using the following format:

Elliott, M. (2026) RaschPy. Downloaded from: https://github.com/MarkElliott999/RaschPy

References

Andrich, D. (1978). A rating formulation for ordered response categories. Psychometrika, 43(4), 561–573.

Choppin, B. (1968). Item bank using sample-free calibration. Nature, 219(5156), 870–872.

Choppin, B. (1985). A fully conditional estimation procedure for Rasch model parameters. Evaluation in Education, 9(1), 29–42.

Elliott, M. (2025). Extended many-facet Rasch models: Accounting for rater effects in automated essay scoring systems (Doctoral dissertation, University of Cambridge).

Elliott, M., & Buttery, P. J. (2022a). Extended rater representations in the many-facet Rasch model. Journal of Applied Measurement, 22(1), 133–160.

Elliott, M., & Buttery, P. J. (2022b). Non-iterative conditional pairwise estimation for the rating scale model. Educational and Psychological Measurement, 82(5), 989–1019.

Garner, M., & Engelhard, G. (2002). An eigenvector method for estimating item parameters of the dichotomous and polytomous Rasch models. Journal of Applied Measurement, 3(2), 107–128.

Garner, M., & Engelhard, G. (2009). Using paired comparison matrices to estimate parameters of the partial credit Rasch measurement model for rater-mediated assessments. Journal of Applied Measurement, 10(1), 30–41.

Linacre, J. M. (1994). Many-Facet Rasch Measurement. MESA Press.

Masters, G. N. (1982). A Rasch model for partial credit scoring. Psychometrika, 47(2), 149–174.

Rasch, G. (1960). Probabilistic models for some intelligence and attainment tests. Danmarks Pædagogiske Institut.

DISCLAIMER

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

raschpy-1.1.1.tar.gz (254.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

raschpy-1.1.1-py3-none-any.whl (254.3 kB view details)

Uploaded Python 3

File details

Details for the file raschpy-1.1.1.tar.gz.

File metadata

  • Download URL: raschpy-1.1.1.tar.gz
  • Upload date:
  • Size: 254.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.11

File hashes

Hashes for raschpy-1.1.1.tar.gz
Algorithm Hash digest
SHA256 f0414f3db4c24dd34911c096951aca007575fee458b125305aa81c007be61b75
MD5 193aa308206f0a5e6647ddaca131d421
BLAKE2b-256 42f5e28794838204da35246ec22fc684e8e474619d4221a28d3ce627da4351af

See more details on using hashes here.

File details

Details for the file raschpy-1.1.1-py3-none-any.whl.

File metadata

  • Download URL: raschpy-1.1.1-py3-none-any.whl
  • Upload date:
  • Size: 254.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.11

File hashes

Hashes for raschpy-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 07ce322233fc08b9dd4d10be15007b0ae3cd7217b20f5fdb0129d12852c38e79
MD5 cf40ee5142a732fc1f3dfb0313c85222
BLAKE2b-256 b866f3cd08d43ad96826ee9da617500360e2f40f3b15a9ff602243a32301b4dd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page