Skip to content

Fix the 0.481 offset #47

Description

@richford

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:

  1. 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.

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions