Source code for edstan.mcmc
from cmdstanpy import CmdStanMCMC
from numpy.typing import NDArray
import numpy as np
import pandas as pd
[docs]
class EdStanMCMC:
"""
A wrapper around :class:`pystan.CmdStanMCMC` that adds additional methods.
This class delegates all unspecified attribute access to the underlying :class:`pystan.CmdStanMCMC` instance via
:meth:`EdStanMCMC.__getattr__`. This allows it to behave like a :class:`pystan.CmdStanMCMC` object while also
providing custom methods.
"""
[docs]
def __init__(
self,
mcmc: CmdStanMCMC,
ii_labels: NDArray,
jj_labels: NDArray,
max_per_item: NDArray[np.integer],
):
"""
Initializes an :class:`EdStanMCMC` instance.
An instance of this class is generated by :meth:`ModelMCMC.sample_from_long` or
:meth:`EdStanModel.sample_from_wide`. Though the class may be initialized directly, this is not the intended
usage.
:param mcmc: A fitted :mod:`edstan` model using MCMC.
:param ii_labels: Labels associated with the items.
:param jj_labels: Labels associated with the persons.
:param max_per_item: The maximum score per item.
"""
self.mcmc = mcmc
self.ii_labels = ii_labels
self.jj_labels = jj_labels
self.max_per_item = max_per_item
def __getattr__(self, name):
return getattr(self.mcmc, name)
[docs]
def item_summary(self, **kwargs):
"""
A wrapper around :meth:`pystan.CmdStanMCMC.summary` that provides posterior summaries grouped by item.
:param kwargs: Additional optional arguments passed to :meth:`pystan.CmdStanMCMC.summary`, such as 'percentiles'
and 'sig_figs'.
:return: A summary :class:`pandas.DataFrame` filtered to include item and distribution parameters only, having a
multi-index that associates parameters with their respective item labels (or the person distribution).
"""
summary = self.mcmc.summary(**kwargs)
summary.index.name = "parameter"
expected = _get_expected_parameters_by_group(
item_labels=self.ii_labels,
max_per_item=self.max_per_item,
rasch_family="sigma" in summary.index,
ratings_model="kappa[1]" in summary.index,
)
return expected.merge(summary.reset_index(), on="parameter").set_index(
["parameter group", "parameter"]
)
[docs]
def person_summary(self, **kwargs):
"""
A wrapper around :meth:pystan.CmdStanMCMC.summary that provides posterior summaries grouped by person.
:param kwargs: Additional optional arguments passed to :meth:`pystan.CmdStanMCMC.summary`, such as 'percentiles'
and 'sig_figs'.
:return: A summary :class:`pandas.DataFrame` filtered to include person parameters only, having a multi-index
that associates parameters with their respective person labels.
"""
summary = self.mcmc.summary(**kwargs)
summary = summary.loc[summary.index.str.match("theta")]
summary["person"] = self.jj_labels
summary.index.name = "parameter"
return summary.reset_index().set_index(["person", "parameter"])
def _get_expected_parameters_by_group(
item_labels, max_per_item, rasch_family: bool, ratings_model: bool
):
"""Generate a DataFrame listing the expected item/distribution parameters and their groupings."""
holder = []
if ratings_model:
betas_per_item = np.ones(len(max_per_item), dtype=int)
else:
betas_per_item = np.array(max_per_item, dtype=int)
beta_counter = 0
for item, item_max in enumerate(betas_per_item):
if not rasch_family:
holder.append((item_labels[item], f"alpha[{item + 1}]"))
for _ in range(item_max):
beta_counter += 1
holder.append((item_labels[item], f"beta[{beta_counter}]"))
if ratings_model:
for j in range(int(max(max_per_item))):
holder.append(("Rating scale steps", f"kappa[{j + 1}]"))
holder.append(("Ability distribution", "lambda[1]"))
if rasch_family:
holder.append(("Ability distribution", "sigma"))
df = pd.DataFrame(holder)
df.columns = ["parameter group", "parameter"]
return df