catpie 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
catpie/__init__.py ADDED
@@ -0,0 +1,193 @@
1
+ """
2
+ catpie - Computerized Adaptive Testing (CAT) in pure Python.
3
+
4
+ A faithful translation of the most relevant subset of the R package ``catR``:
5
+ the 4PL item response function, EAP/BM/ML/WL ability estimation with standard
6
+ errors, and MFI/bOpt item selection, plus the simulation helpers
7
+ ``genPattern``, ``checkStopRule`` and a minimal ``randomCAT`` loop.
8
+
9
+ This is a Python port of the R package ``catR`` (and mirrors the sibling
10
+ JavaScript port ``catjs-irt``). It is not written or endorsed by the catR
11
+ authors. Numbers are computed with the exact same formulas and grid as catR
12
+ (defaults: D=1, priorDist="norm", priorPar=(0,1), parInt=(-4,4,33), trapezoid
13
+ integration over 33 points), so results match catR to floating point precision
14
+ for the same inputs.
15
+
16
+ An "item" is a 4-tuple ``(a, b, c, d)`` (discrimination, difficulty, guessing,
17
+ inattention). Item indices are 0-indexed throughout the public API.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import NamedTuple, Optional, Sequence, Tuple
23
+
24
+ from .eap import eapEst, eapSem
25
+ from .estimators import semTheta, thetaEst
26
+ from .irf import IiResult, Item, JiResult, PiResult, ii, ji, pi
27
+ from .math import (
28
+ OptimizeResult,
29
+ dnorm,
30
+ integrateCatR,
31
+ linspace,
32
+ optimizeScalar,
33
+ qnorm,
34
+ uniroot,
35
+ )
36
+ from .selection import NextItemResult, nextItem
37
+ from .simulation import (
38
+ RandomCatResult,
39
+ StopRuleResult,
40
+ checkStopRule,
41
+ genPattern,
42
+ randomCAT,
43
+ simulateRespondents,
44
+ )
45
+
46
+ __version__ = "0.1.0"
47
+
48
+ __all__ = [
49
+ # IRF
50
+ "pi",
51
+ "ii",
52
+ "ji",
53
+ "PiResult",
54
+ "IiResult",
55
+ "JiResult",
56
+ # estimation
57
+ "eapEst",
58
+ "eapSem",
59
+ "thetaEst",
60
+ "semTheta",
61
+ # selection
62
+ "nextItem",
63
+ "NextItemResult",
64
+ # simulation
65
+ "genPattern",
66
+ "simulateRespondents",
67
+ "checkStopRule",
68
+ "randomCAT",
69
+ "StopRuleResult",
70
+ "RandomCatResult",
71
+ # high-level helpers
72
+ "estimateTheta",
73
+ "selectNextItem",
74
+ "ThetaResult",
75
+ # math helpers (mirror catR/R base functions)
76
+ "dnorm",
77
+ "linspace",
78
+ "integrateCatR",
79
+ "qnorm",
80
+ "uniroot",
81
+ "optimizeScalar",
82
+ "OptimizeResult",
83
+ "__version__",
84
+ ]
85
+
86
+
87
+ class ThetaResult(NamedTuple):
88
+ """Ability estimate and its standard error (high-level helper)."""
89
+
90
+ theta: float
91
+ se: float
92
+
93
+
94
+ def estimateTheta(
95
+ itemBank: Sequence[Item],
96
+ administered: Sequence[int],
97
+ responses: Sequence[int],
98
+ method: str = "EAP",
99
+ priorDist: str = "norm",
100
+ priorPar: Tuple[float, float] = (0, 1),
101
+ D: float = 1.0,
102
+ range: Tuple[float, float] = (-4, 4),
103
+ parInt: Tuple[float, float, int] = (-4, 4, 33),
104
+ semType: str = "classic",
105
+ ) -> ThetaResult:
106
+ """
107
+ Estimate ability and its standard error from the items given so far.
108
+
109
+ This is the highest-level estimation helper: hand it the whole item bank,
110
+ the indices of the items already administered, and the person's 0/1
111
+ responses, and you get back ``(theta, se)`` in one call. It is exactly the
112
+ helper the EWM experiment's ``estimate_theta_catr(...)`` uses.
113
+
114
+ Args:
115
+ itemBank: The full item bank, as ``(a, b, c, d)`` tuples.
116
+ administered: 0-based indices (into ``itemBank``) of the items
117
+ administered so far.
118
+ responses: 0/1 responses in the same order as ``administered``.
119
+ method: Estimation method (default ``"EAP"``; also ``"BM"``,
120
+ ``"ML"``, ``"WL"``).
121
+ priorDist: Prior distribution (``"norm"`` default).
122
+ priorPar: Prior parameters (default ``(0, 1)``).
123
+ D: Scale constant (default ``1.0``).
124
+ range: Search interval for BM/ML/WL (default ``(-4, 4)``).
125
+ parInt: EAP integration grid (default ``(-4, 4, 33)``).
126
+ semType: ``"classic"`` (default) SE form.
127
+
128
+ Returns:
129
+ A :class:`ThetaResult` with fields ``theta`` and ``se``. If no items
130
+ have been administered yet, returns the prior estimate ``(0.0, inf)``.
131
+ """
132
+ if len(administered) == 0:
133
+ return ThetaResult(0.0, float("inf"))
134
+ it = [itemBank[i] for i in administered]
135
+ theta = thetaEst(
136
+ it,
137
+ responses,
138
+ method=method,
139
+ priorDist=priorDist,
140
+ priorPar=priorPar,
141
+ D=D,
142
+ range=range,
143
+ parInt=parInt,
144
+ )
145
+ se = semTheta(
146
+ theta,
147
+ it,
148
+ responses,
149
+ method=method,
150
+ priorDist=priorDist,
151
+ priorPar=priorPar,
152
+ D=D,
153
+ parInt=parInt,
154
+ semType=semType,
155
+ )
156
+ return ThetaResult(theta, se)
157
+
158
+
159
+ def selectNextItem(
160
+ itemBank: Sequence[Item],
161
+ theta: float,
162
+ administered: Sequence[int],
163
+ criterion: str = "MFI",
164
+ randomesque: int = 1,
165
+ D: float = 1.0,
166
+ ) -> NextItemResult:
167
+ """
168
+ Select the next item to administer (default criterion: MFI).
169
+
170
+ High-level wrapper around :func:`nextItem` that makes the most common call
171
+ shorter. It is exactly the helper the EWM experiment's
172
+ ``select_next_item_catr(...)`` uses.
173
+
174
+ Args:
175
+ itemBank: The full item bank, as ``(a, b, c, d)`` tuples.
176
+ theta: Current ability estimate (a float, typically ``[-4, 4]``).
177
+ administered: 0-based indices of the items already administered (they
178
+ will not be selected again).
179
+ criterion: ``"MFI"`` (default) or ``"bOpt"``.
180
+ randomesque: Only the catR default ``1`` is supported.
181
+ D: Scale constant (default ``1.0``).
182
+
183
+ Returns:
184
+ A :class:`NextItemResult`; see :func:`nextItem` for its fields.
185
+ """
186
+ return nextItem(
187
+ itemBank,
188
+ theta,
189
+ administered,
190
+ criterion=criterion,
191
+ randomesque=randomesque,
192
+ D=D,
193
+ )
catpie/eap.py ADDED
@@ -0,0 +1,158 @@
1
+ """
2
+ Expected A Posteriori (EAP) estimation, mirroring catR's ``eapEst()`` and
3
+ ``eapSem()`` exactly (including the grid, the prior and the trapezoid
4
+ integration via ``integrate.catR``).
5
+
6
+ Supported priors (catR's ``priorDist``): "norm" (N(priorPar[0], priorPar[1])),
7
+ "unif" (uniform over priorPar), "Jeffreys" (weight sqrt(sum(Ii))).
8
+
9
+ This mirrors catjs-irt's ``src/eap.js``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import math
15
+ from typing import Callable, Sequence, Tuple
16
+
17
+ from .irf import Item, ii, pi
18
+ from .math import dnorm, integrateCatR, linspace
19
+
20
+
21
+ def priorWeight(
22
+ s: float,
23
+ priorDist: str,
24
+ priorPar: Tuple[float, float],
25
+ it: Sequence[Item],
26
+ D: float = 1.0,
27
+ ) -> float:
28
+ """
29
+ Prior density term used in the EAP integrand, matching catR's
30
+ ``switch(...)`` inside ``eapEst``/``eapSem``. For "norm" returns
31
+ ``dnorm(s)``; for "unif" returns ``dunif(s)`` (1/(b-a) inside [a, b], 0
32
+ outside); for "Jeffreys" returns ``sqrt(sum(Ii(s, it)))``.
33
+ """
34
+ if priorDist == "norm":
35
+ return dnorm(s, priorPar[0], priorPar[1])
36
+ if priorDist == "unif":
37
+ a, b = priorPar[0], priorPar[1]
38
+ return 1.0 / (b - a) if a <= s <= b else 0.0
39
+ if priorDist == "Jeffreys":
40
+ return math.sqrt(sum(ii(s, item, D).Ii for item in it))
41
+ raise ValueError("eapEst: priorDist %r not implemented" % (priorDist,))
42
+
43
+
44
+ def makeLikelihood(
45
+ it: Sequence[Item], x: Sequence[int], D: float = 1.0
46
+ ) -> Callable[[float], float]:
47
+ """
48
+ Build the likelihood L(th) = prod_i P_i^x_i * (1 - P_i)^(1 - x_i),
49
+ exactly as in catR's ``eapEst``:
50
+
51
+ L <- function(th, it, x) prod(Pi(th, it, D=D)$Pi^x *
52
+ (1-Pi(th, it, D=D)$Pi)^(1-x))
53
+ """
54
+ def likelihood(th: float) -> float:
55
+ res = 1.0
56
+ for i, item in enumerate(it):
57
+ P = pi(th, item, D).P
58
+ res *= P ** x[i] * (1 - P) ** (1 - x[i])
59
+ return res
60
+
61
+ return likelihood
62
+
63
+
64
+ def eapEst(
65
+ it: Sequence[Item],
66
+ x: Sequence[int],
67
+ D: float = 1.0,
68
+ priorDist: str = "norm",
69
+ priorPar: Tuple[float, float] = (0, 1),
70
+ lower: float = -4,
71
+ upper: float = 4,
72
+ nqp: int = 33,
73
+ ) -> float:
74
+ """
75
+ Ability estimate by Expected A Posteriori (EAP) estimation.
76
+
77
+ EAP is a *Bayesian* estimate: it combines what the responses tell us (the
78
+ likelihood) with a *prior* belief about the ability distribution. It is the
79
+ most robust of the implemented estimators and the default choice of this
80
+ package (and of the EWM experiment).
81
+
82
+ The estimate is computed by numerical integration over a grid of ability
83
+ values (catR's default: 33 points from ``-4`` to ``4``), so the result is
84
+ always within ``[lower, upper]``.
85
+
86
+ Args:
87
+ it: The items that were administered, as a list of ``(a, b, c, d)``
88
+ tuples. Length must equal ``len(x)``.
89
+ x: The responses, as 0/1 integers (1 = correct) in the same order as
90
+ ``it``.
91
+ D: Scale constant (default ``1.0``).
92
+ priorDist: The prior distribution, one of ``"norm"`` (normal),
93
+ ``"unif"`` (uniform) or ``"Jeffreys"``.
94
+ priorPar: Prior parameters. For ``"norm"``: ``(mean, sd)`` — catR
95
+ default ``(0, 1)`` (standard normal). For ``"unif"``: ``(lower,
96
+ upper)`` of the uniform interval.
97
+ lower: Lower bound of the integration grid (default ``-4``).
98
+ upper: Upper bound of the integration grid (default ``4``).
99
+ nqp: Number of quadrature points in the grid (catR default ``33``).
100
+
101
+ Returns:
102
+ The estimated ability ``theta`` (a float, always within
103
+ ``[lower, upper]``).
104
+ """
105
+ L = makeLikelihood(it, x, D)
106
+ X = linspace(lower, upper, nqp)
107
+
108
+ def w(s: float) -> float:
109
+ return priorWeight(s, priorDist, priorPar, it, D)
110
+
111
+ g = [s * w(s) * L(s) for s in X]
112
+ h = [w(s) * L(s) for s in X]
113
+ return integrateCatR(X, g) / integrateCatR(X, h)
114
+
115
+
116
+ def eapSem(
117
+ thEst: float,
118
+ it: Sequence[Item],
119
+ x: Sequence[int],
120
+ D: float = 1.0,
121
+ priorDist: str = "norm",
122
+ priorPar: Tuple[float, float] = (0, 1),
123
+ lower: float = -4,
124
+ upper: float = 4,
125
+ nqp: int = 33,
126
+ ) -> float:
127
+ """
128
+ Standard error of an EAP ability estimate.
129
+
130
+ The standard error tells you how confident you can be in the ability
131
+ estimate :func:`eapEst`: smaller = more confidence. It is the standard
132
+ deviation of the posterior distribution, computed by the same numerical
133
+ integration over the grid.
134
+
135
+ Args:
136
+ thEst: The ability estimate (the output of :func:`eapEst`).
137
+ it: The administered items, as ``(a, b, c, d)`` tuples.
138
+ x: The responses (0/1), same order as ``it``.
139
+ D: Scale constant (default ``1.0``).
140
+ priorDist: Prior distribution, ``"norm"`` / ``"unif"`` / ``"Jeffreys"``.
141
+ priorPar: Prior parameters (see :func:`eapEst`).
142
+ lower: Lower bound of the integration grid (default ``-4``).
143
+ upper: Upper bound of the integration grid (default ``4``).
144
+ nqp: Number of quadrature points (catR default ``33``).
145
+
146
+ Returns:
147
+ The standard error (a non-negative float; typically a few tenths, and
148
+ smaller after more informative items are administered).
149
+ """
150
+ L = makeLikelihood(it, x, D)
151
+ X = linspace(lower, upper, nqp)
152
+
153
+ def w(s: float) -> float:
154
+ return priorWeight(s, priorDist, priorPar, it, D)
155
+
156
+ g = [(s - thEst) ** 2 * w(s) * L(s) for s in X]
157
+ h = [w(s) * L(s) for s in X]
158
+ return math.sqrt(integrateCatR(X, g) / integrateCatR(X, h))
catpie/estimators.py ADDED
@@ -0,0 +1,312 @@
1
+ """
2
+ Ability (theta) estimation and its standard error, mirroring catR's
3
+ ``thetaEst()`` and ``semTheta()`` for the dichotomous model:
4
+
5
+ - method: "EAP" | "BM" | "ML" | "WL"
6
+ - priorDist: "norm" | "unif" | "Jeffreys" (used by EAP and BM)
7
+ - defaults: D=1, priorPar=(0,1), parInt=(-4,4,33), range=(-4,4)
8
+
9
+ The non-EAP methods replicate catR's exact algorithm: solve the score equation
10
+ T(th) = r0(th) + r(th) = 0 by bisection (R's ``uniroot``) over ``range``, with
11
+ catR's ``optimize()``-based fallback when T does not change sign.
12
+
13
+ This mirrors catjs-irt's ``src/estimators.js``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import math
19
+ from typing import NamedTuple, Optional, Sequence, Tuple, Union
20
+
21
+ from .eap import eapEst, eapSem
22
+ from .irf import Item, ii, ji, pi
23
+ from .math import OptimizeResult, _sqrt, optimizeScalar, uniroot
24
+
25
+ Number = Union[int, float]
26
+
27
+
28
+ class SumDersResult(NamedTuple):
29
+ """Summed information and its derivatives over a set of items."""
30
+
31
+ Ii: float
32
+ dIi: float
33
+ d2Ii: float
34
+
35
+
36
+ def _is_missing(v: object) -> bool:
37
+ """True for NaN/None, matching catjs-irt's NaN/undefined/null filter."""
38
+ if v is None:
39
+ return True
40
+ try:
41
+ return math.isnan(v) # type: ignore[arg-type]
42
+ except TypeError:
43
+ return False
44
+
45
+
46
+ def dropMissing(
47
+ it: Sequence[Item], x: Sequence[Number]
48
+ ) -> Tuple[Sequence[Item], Sequence[Number]]:
49
+ """
50
+ Filter out NaN/None responses and the corresponding items, matching catR's
51
+ ``thetaEst``: ``ind <- which(!is.na(x)); it <- it[ind, ]; x <- x[ind]``.
52
+ """
53
+ idx = [i for i, v in enumerate(x) if not _is_missing(v)]
54
+ return [it[i] for i in idx], [x[i] for i in idx]
55
+
56
+
57
+ def sumIi(th: float, it: Sequence[Item], D: float) -> float:
58
+ """Sum of item information at ``th`` over ``it``."""
59
+ return sum(ii(th, item, D).Ii for item in it)
60
+
61
+
62
+ def sumDers(th: float, it: Sequence[Item], D: float) -> SumDersResult:
63
+ """Sum of item information derivatives at ``th`` over ``it``."""
64
+ Ii = 0.0
65
+ dIi = 0.0
66
+ d2Ii = 0.0
67
+ for item in it:
68
+ res = ii(th, item, D)
69
+ Ii += res.Ii
70
+ dIi += res.dIi
71
+ d2Ii += res.d2Ii
72
+ return SumDersResult(Ii, dIi, d2Ii)
73
+
74
+
75
+ def r0(
76
+ th: float,
77
+ it: Sequence[Item],
78
+ method: str = "BM",
79
+ priorDist: str = "norm",
80
+ priorPar: Tuple[float, float] = (0, 1),
81
+ D: float = 1.0,
82
+ ) -> float:
83
+ """catR's ``r0`` term (prior / method correction) in the score equation."""
84
+ if method == "ML":
85
+ return 0.0
86
+ if method == "WL":
87
+ sumJ = sum(ji(th, item, D).Ji for item in it)
88
+ return sumJ / (2 * sumIi(th, it, D))
89
+ # BM
90
+ if priorDist == "norm":
91
+ return (priorPar[0] - th) / priorPar[1] ** 2
92
+ if priorDist == "unif":
93
+ return 0.0
94
+ if priorDist == "Jeffreys":
95
+ ders = sumDers(th, it, D)
96
+ return ders.dIi / (2 * ders.Ii)
97
+ raise ValueError("thetaEst: priorDist %r not implemented" % (priorDist,))
98
+
99
+
100
+ def r(th: float, it: Sequence[Item], x: Sequence[Number], D: float = 1.0) -> float:
101
+ """catR's ``r`` term: sum of dP*(x - P)/(P*Q) over items."""
102
+ res = 0.0
103
+ for i, item in enumerate(it):
104
+ pr = pi(th, item, D)
105
+ P = pr.P
106
+ Q = 1 - P
107
+ dP = pr.dP
108
+ res += (dP * (x[i] - P)) / (P * Q)
109
+ return res
110
+
111
+
112
+ def thetaEst(
113
+ it: Sequence[Item],
114
+ x: Sequence[Number],
115
+ method: str = "EAP",
116
+ priorDist: str = "norm",
117
+ priorPar: Tuple[float, float] = (0, 1),
118
+ D: float = 1.0,
119
+ range: Tuple[float, float] = (-4, 4),
120
+ parInt: Tuple[float, float, int] = (-4, 4, 33),
121
+ ) -> float:
122
+ """
123
+ Estimate a person's ability (theta) from their responses.
124
+
125
+ This is the main estimation function. Give it the items that were
126
+ administered and the person's 0/1 responses, and it returns the best
127
+ estimate of their ability. The higher the ability, the "better" the
128
+ person performed.
129
+
130
+ Four estimation methods are available:
131
+
132
+ - ``"EAP"`` (default): Bayesian, most robust, uses the prior; the result
133
+ always lies within the integration grid (default ``[-4, 4]``).
134
+ - ``"BM"``: Bayesian modal estimate (posterior maximum), uses the prior.
135
+ - ``"ML"``: maximum likelihood; uses only the responses (no prior). Can
136
+ be extreme (and is unstable on degenerate item banks, exactly like catR).
137
+ - ``"WL"``: weighted likelihood; reduces the bias of ML.
138
+
139
+ Missing responses (``NaN``/``None``) and their items are ignored.
140
+
141
+ Args:
142
+ it: The administered items as ``(a, b, c, d)`` tuples (same length as
143
+ ``x``).
144
+ x: The responses as 0/1 integers (1 = correct); ``NaN``/``None``
145
+ entries are dropped together with the corresponding items.
146
+ method: One of ``"EAP"`` (default), ``"BM"``, ``"ML"``, ``"WL"``.
147
+ priorDist: Prior distribution for ``EAP``/``BM``: ``"norm"``
148
+ (default), ``"unif"``, ``"Jeffreys"``.
149
+ priorPar: Prior parameters ``(mean, sd)`` for ``"norm"`` (default
150
+ ``(0, 1)``) or ``(lower, upper)`` for ``"unif"``.
151
+ D: Scale constant (default ``1.0``).
152
+ range: The search interval ``(lower, upper)`` for the BM/ML/WL root
153
+ finding (default ``(-4, 4)``). Not used by EAP.
154
+ parInt: ``(lower, upper, npoints)`` of the integration grid for EAP
155
+ (default ``(-4, 4, 33)``).
156
+
157
+ Returns:
158
+ The estimated ability as a float (a higher value = higher estimated
159
+ ability). For EAP it lies within the integration grid; BM/ML/WL may
160
+ return a range boundary when no interior root exists (like catR).
161
+
162
+ Raises:
163
+ ValueError: If ``method`` is not one of EAP/BM/ML/WL, or ``priorDist``
164
+ is not supported.
165
+ """
166
+ fit, fx = dropMissing(it, x)
167
+
168
+ if method == "EAP":
169
+ return eapEst(
170
+ fit,
171
+ fx,
172
+ D=D,
173
+ priorDist=priorDist,
174
+ priorPar=priorPar,
175
+ lower=parInt[0],
176
+ upper=parInt[1],
177
+ nqp=parInt[2],
178
+ )
179
+ if method not in ("BM", "ML", "WL"):
180
+ raise ValueError("thetaEst: method %r not implemented" % (method,))
181
+
182
+ # catR: f is T(th) = r0(th) + r(th), except BM+unif which reduces to ML
183
+ # and searches over the prior interval.
184
+ usePrior = not (method == "BM" and priorDist == "unif")
185
+
186
+ def f(th: float) -> float:
187
+ t = r(th, fit, fx, D)
188
+ if usePrior:
189
+ return (
190
+ r0(
191
+ th,
192
+ fit,
193
+ method=method,
194
+ priorDist=priorDist,
195
+ priorPar=priorPar,
196
+ D=D,
197
+ )
198
+ + t
199
+ )
200
+ return t
201
+
202
+ RANGE = priorPar if (method == "BM" and priorDist == "unif") else range
203
+
204
+ fLo = f(RANGE[0])
205
+ fHi = f(RANGE[1])
206
+ if (fLo < 0 < fHi) or (fLo > 0 > fHi):
207
+ return uniroot(f, RANGE[0], RANGE[1])
208
+
209
+ # catR fallback: minimize f; if min > 0 -> upper bound; maximize; if max < 0
210
+ # -> lower bound; else root between the argmax and argmin.
211
+ pr: OptimizeResult = optimizeScalar(f, RANGE[0], RANGE[1])
212
+ if pr.y > 0:
213
+ return RANGE[1]
214
+ pr2: OptimizeResult = optimizeScalar(f, RANGE[0], RANGE[1], maximize=True)
215
+ if pr2.y < 0:
216
+ return RANGE[0]
217
+ lo = min(pr2.x, pr.x)
218
+ hi = max(pr2.x, pr.x)
219
+ return uniroot(f, lo, hi)
220
+
221
+
222
+ def semTheta(
223
+ thEst: float,
224
+ it: Sequence[Item],
225
+ x: Sequence[Number],
226
+ method: str = "EAP",
227
+ priorDist: str = "norm",
228
+ priorPar: Tuple[float, float] = (0, 1),
229
+ D: float = 1.0,
230
+ parInt: Tuple[float, float, int] = (-4, 4, 33),
231
+ semType: str = "classic",
232
+ range: Tuple[float, float] = (-4, 4),
233
+ ) -> float:
234
+ """
235
+ Standard error (SE) of an ability estimate.
236
+
237
+ The SE quantifies the uncertainty of an ability estimate :func:`thetaEst`:
238
+ small SE = confident estimate, large SE = uncertain. It is used by the
239
+ stopping rules (e.g. stop once ``se`` drops below a threshold) and to build
240
+ confidence intervals for classification.
241
+
242
+ The method should match the one used for estimation (``method=...``).
243
+
244
+ Args:
245
+ thEst: The ability estimate (output of :func:`thetaEst`).
246
+ it: The administered items as ``(a, b, c, d)`` tuples.
247
+ x: The responses (0/1); ``NaN``/``None`` are dropped with their items.
248
+ method: Estimation method: ``"EAP"`` (default), ``"BM"``, ``"ML"``,
249
+ ``"WL"``.
250
+ priorDist: Prior distribution for EAP/BM (``"norm"`` default,
251
+ ``"unif"``, ``"Jeffreys"``).
252
+ priorPar: Prior parameters (see :func:`thetaEst`).
253
+ D: Scale constant (default ``1.0``).
254
+ parInt: Integration grid ``(lower, upper, npoints)`` for EAP (default
255
+ ``(-4, 4, 33)``).
256
+ semType: ``"classic"`` (default) or ``"new"`` (only implemented for
257
+ the classic form in this port).
258
+ range: Accepted for catR signature parity; not used by the implemented
259
+ methods.
260
+
261
+ Returns:
262
+ The standard error as a non-negative float. As a rough guide, values
263
+ below ~0.3 are often considered "precise enough" for adaptive testing.
264
+
265
+ Raises:
266
+ ValueError: If ``method`` is not one of EAP/BM/ML/WL or ``priorDist``
267
+ is not supported.
268
+ """
269
+ fit, fx = dropMissing(it, x)
270
+
271
+ if method == "EAP":
272
+ return eapSem(
273
+ thEst,
274
+ fit,
275
+ fx,
276
+ D=D,
277
+ priorDist=priorDist,
278
+ priorPar=priorPar,
279
+ lower=parInt[0],
280
+ upper=parInt[1],
281
+ nqp=parInt[2],
282
+ )
283
+ if method not in ("BM", "ML", "WL"):
284
+ raise ValueError("semTheta: method %r not implemented" % (method,))
285
+
286
+ info = sumIi(thEst, fit, D)
287
+
288
+ if method == "ML":
289
+ return 1.0 / _sqrt(info)
290
+
291
+ if method == "WL":
292
+ # classic: 1/sqrt(info); new: sqrt(info)/abs(info - dr0) -- classic only
293
+ # is implemented (the catR new-type WL needs Ji derivatives).
294
+ if semType == "new":
295
+ dr0wl = 0.0
296
+ return _sqrt(info) / abs(info - dr0wl)
297
+ return 1.0 / _sqrt(info)
298
+
299
+ # BM
300
+ if priorDist == "norm":
301
+ dr0 = -1.0 / priorPar[1] ** 2
302
+ elif priorDist == "unif":
303
+ dr0 = 0.0
304
+ elif priorDist == "Jeffreys":
305
+ ders = sumDers(thEst, fit, D)
306
+ dr0 = (ders.d2Ii * ders.Ii - ders.dIi * ders.dIi) / (2 * ders.Ii * ders.Ii)
307
+ else:
308
+ raise ValueError("semTheta: priorDist %r not implemented" % (priorDist,))
309
+
310
+ if semType == "classic":
311
+ return 1.0 / _sqrt(info - dr0)
312
+ return _sqrt(info) / abs(info - dr0)