The constant 0.481 was introduced in 2022 by commit 8c76113 ("add more methods"). For example, selectorClosest does:
const target = this._theta + 0.481;
Three things are wrong with this line:
-
It's not from catR, despite the project's stated alignment with catR. catR's bOpt rule (the closest-difficulty equivalent) uses abs(itemBank[, 2] - theta). No offset. catR's thOpt rule does derive an optimal-θ per item, but via a per-item cubic over the actual (a, b, c, d), not a global constant.
-
It's a magic number with a hidden assumption. 0.481 ≈ ln((1 + √5)/2) = ln(φ), which is the canonical 3PL maximum-information offset under c = 0.5, a = 1, D = 1. Hardcoding it bakes in those parameter assumptions for every item in every corpus, which silently breaks for any item with different c or a.
-
I'm not sure about this one but to me the sign appears to be wrong. For an item at b = 0, c = 0.5, Fisher information is maximized at θ ≈ **+**0.481. By translation, the optimal item for an examinee at θ_e has b = θ_e − 0.481 (an easier item to compensate for guessing). The current code does target = theta + 0.481, which selects items in the opposite direction. Confirmed numerically:
item at b=0, c=0.5:
I(theta=-0.481) = 0.0653
I(theta= 0.000) = 0.0833
I(theta=+0.481) = 0.0902 <- max
I(theta=+1.000) = 0.0830
So an examinee at θ=0 gets the most information from an item with b ≈ -0.481, but selectorClosest targets b ≈ +0.481.
Suggested fix
Pick one of:
- Match catR's
bOpt: drop the offset entirely. target = this._theta.
- Match catR's
thOpt: compute a per-item thstar from each item's (a, b, c, d) via the cubic in catR's source, then find the item whose thstar is closest to θ. Most faithful, most work.
- Keep a c-aware offset but fix the sign and parameterize it: for the 3PL, the optimal target b for examinee at θ is
θ - (1/Da) * ln((1 + sqrt(1 + 8c))/2). This recovers b = θ for c=0 and gives b = θ - 0.481 for c=0.5, a=1. Compute per-item rather than as a global constant.
In any of these, extract any unavoidable constants into named values (e.g., OPTIMAL_3PL_OFFSET_C_HALF with a derivation comment) so they aren't naked literals in the selection logic.
The constant 0.481 was introduced in 2022 by commit 8c76113 ("add more methods"). For example,
selectorClosestdoes:Three things are wrong with this line:
It's not from catR, despite the project's stated alignment with catR. catR's
bOptrule (the closest-difficulty equivalent) usesabs(itemBank[, 2] - theta). No offset. catR'sthOptrule does derive an optimal-θ per item, but via a per-item cubic over the actual(a, b, c, d), not a global constant.It's a magic number with a hidden assumption.
0.481 ≈ ln((1 + √5)/2) = ln(φ), which is the canonical 3PL maximum-information offset underc = 0.5, a = 1, D = 1. Hardcoding it bakes in those parameter assumptions for every item in every corpus, which silently breaks for any item with differentcora.I'm not sure about this one but to me the sign appears to be wrong. For an item at
b = 0, c = 0.5, Fisher information is maximized at θ ≈ **+**0.481. By translation, the optimal item for an examinee at θ_e hasb = θ_e − 0.481(an easier item to compensate for guessing). The current code doestarget = theta + 0.481, which selects items in the opposite direction. Confirmed numerically:So an examinee at θ=0 gets the most information from an item with b ≈ -0.481, but
selectorClosesttargets b ≈ +0.481.Suggested fix
Pick one of:
bOpt: drop the offset entirely.target = this._theta.thOpt: compute a per-itemthstarfrom each item's(a, b, c, d)via the cubic in catR's source, then find the item whosethstaris closest to θ. Most faithful, most work.θ - (1/Da) * ln((1 + sqrt(1 + 8c))/2). This recoversb = θfor c=0 and givesb = θ - 0.481for c=0.5, a=1. Compute per-item rather than as a global constant.In any of these, extract any unavoidable constants into named values (e.g.,
OPTIMAL_3PL_OFFSET_C_HALFwith a derivation comment) so they aren't naked literals in the selection logic.