Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 1PLG #10

Merged
merged 13 commits into from
May 24, 2024
Merged
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
SimpleUnPack = "ce78b400-467f-4804-87d8-8f486da07d0a"

[compat]
julia = "1.8"
AbstractItemResponseModels = "0.2"
DocStringExtensions = "0.9"
LogExpFunctions = "0.3"
Reexport = "1"
SimpleUnPack = "1"
julia = "1.8"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
2 changes: 2 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CurrentModule = ItemResponseFunctions
## Types
```@docs
OneParameterLogisticModel
OneParameterLogisticPlusGuessingModel
TwoParameterLogisticModel
ThreeParameterLogisticModel
FourParameterLogisticModel
Expand All @@ -19,6 +20,7 @@ GeneralizedRatingScaleModel
## Functions
```@docs
irf
irf!
iif
expected_score
information
Expand Down
42 changes: 42 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,45 @@
[![Coverage](https://codecov.io/gh/p-gw/ItemResponseFunctions.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/p-gw/ItemResponseFunctions.jl)

[ItemResponseFunctions.jl](https://github.com/p-gw/ItemResponseFunctions.jl) implements basic functions for Item Response Theory models. It is built based on the interface designed in [AbstractItemResponseModels.jl](https://github.com/JuliaPsychometrics/AbstractItemResponseModels.jl).

## Installation
You can install ItemResponseFunctions.jl from Github

```julia
] add https://github.com/p-gw/ItemResponseFunctions.jl.git
```

## Usage
ItemResponseFunctions.jl exports the following functions for Item Response Theory models:

- `irf`: The item response function
- `iif`: The item information function
- `expected_score`: The expected score / test response function
- `information`: The test information function

Calling the function requires a model type `M`, a person ability `theta` and item parameters `beta`.
For a simple 1-Parameter Logistic model,

```julia
using ItemResponseFunctions

beta = (; b = 0.5)

irf(OnePL, 0.0, beta)
iif(OnePL, 0.0, beta)
```

evaluates the item response function and item information function at ability value `0.0` for an item with difficulty `0.5`.

Given an array of item parameters (a test) and an ability value, the test response function and test information can be calculated by

```julia
betas = [
(; b = -0.3),
(; b = 0.25),
(; b = 1.0),
]

expected_score(OnePL, 0.0, betas)
information(OnePL, 0.0, betas)
```
6 changes: 5 additions & 1 deletion src/ItemResponseFunctions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export DichotomousItemResponseModel,
FourParameterLogisticModel,
OnePL,
OneParameterLogisticModel,
OnePLG,
OneParameterLogisticPlusGuessingModel,
ThreePL,
ThreeParameterLogisticModel,
TwoPL,
Expand All @@ -28,9 +30,11 @@ export DichotomousItemResponseModel,
RatingScaleModel,
GRSM,
GeneralizedRatingScaleModel,
partial_credit
partial_credit,
irf!

include("model_types.jl")
include("utils.jl")
include("irf.jl")
include("iif.jl")
include("expected_score.jl")
Expand Down
42 changes: 29 additions & 13 deletions src/expected_score.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ julia> expected_score(FourPL, 0.0, betas)

"""
function expected_score(
M::Type{<:DichotomousItemResponseModel},
theta::T,
M::Type{<:ItemResponseModel},
theta,
betas::AbstractVector;
scoring_function::F = identity,
) where {T<:Real,F}
score = zero(T)
) where {F}
score = zero(theta)

for beta in betas
score += expected_score(M, theta, beta; scoring_function)
Expand All @@ -71,27 +71,32 @@ end

function expected_score(
M::Type{<:DichotomousItemResponseModel},
theta::T,
beta;
theta,
beta::Union{<:Real,NamedTuple};
scoring_function::F = identity,
) where {T<:Real,F}
score = zero(T)
) where {F}
score = zero(theta)

for y in 0:1
score += irf(M, theta, beta, y) * scoring_function(y)
end

return score
end

# for models with non-item specific thresholds vector holding category probabilities can be
# pre-allocated
function expected_score(
M::Type{<:PolytomousItemResponseModel},
M::Type{<:Union{RSM,GRSM}},
theta::T,
betas::AbstractVector;
scoring_function::F = identity,
) where {T<:Real,F}
score = zero(T)
probs = zeros(T, length(first(betas).t) + 1)

for beta in betas
score += expected_score(M, theta, beta; scoring_function)
score += _expected_score(M, probs, theta, beta; scoring_function)
end

return score
Expand All @@ -100,11 +105,22 @@ end
function expected_score(
M::Type{<:PolytomousItemResponseModel},
theta::T,
beta;
beta::NamedTuple;
scoring_function::F = identity,
) where {T<:Real,F}
score = zero(T)
probs = irf(M, theta, beta)
probs = zeros(T, length(beta.t) + 1)
return _expected_score(M, probs, theta, beta; scoring_function)
end

function _expected_score(
M::Type{<:PolytomousItemResponseModel},
probs,
theta,
beta::NamedTuple;
scoring_function::F = identity,
) where {F}
score = zero(theta)
irf!(M, probs, theta, beta)

for (category, prob) in enumerate(probs)
score += prob * scoring_function(category)
Expand Down
76 changes: 30 additions & 46 deletions src/iif.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,6 @@ julia> iif(FourPL, 0.0, (a = 2.1, b = -1.5, c = 0.15, d = 0.9))
```

"""
function iif(M::Type{OnePL}, theta::Real, beta::Real, y = 1)
prob = irf(M, theta, beta, y)
return prob * (1 - prob)
end

function iif(M::Type{OnePL}, theta, beta, y = 1)
return iif(FourPL, theta, merge(beta, (a = 1.0, c = 0.0, d = 1.0)), y)
end

function iif(M::Type{TwoPL}, theta, beta, y = 1)
return iif(FourPL, theta, merge(beta, (; c = 0.0, d = 1.0)), y)
end

function iif(M::Type{ThreePL}, theta, beta, y = 1)
return iif(FourPL, theta, merge(beta, (; d = 1.0)), y)
end

function iif(M::Type{FourPL}, theta, beta::NamedTuple, y = 1)
@unpack a, b, c, d = beta
prob = irf(M, theta, beta, y)
Expand All @@ -65,10 +48,19 @@ function iif(M::Type{FourPL}, theta, beta::NamedTuple, y = 1)
return info
end

"""
$(SIGNATURES)
"""
function iif(M::Type{<:DichotomousItemResponseModel}, theta, beta, y = 1)
pars = merge_pars(M, beta)
return iif(FourPL, theta, pars, y)
end

function iif(M::Type{OnePL}, theta::Real, beta::Real, y = 1)
prob = irf(M, theta, beta, y)
return prob * (1 - prob)
end

function iif(M::Type{GPCM}, theta, beta; scoring_function::F = identity) where {F}
@unpack a = beta

probs = irf(M, theta, beta)
score = expected_score(M, theta, beta)

Expand All @@ -78,36 +70,28 @@ function iif(M::Type{GPCM}, theta, beta; scoring_function::F = identity) where {
info += (scoring_function(category) - score)^2 * prob
end

info *= beta.a^2
info *= a^2

return info
end

function iif(M::Type{GPCM}, theta, beta, y; scoring_function::F = identity) where {F}
prob = irf(M, theta, beta, y)
return prob * iif(M, theta, beta; scoring_function)
end

function iif(M::Type{PCM}, theta, beta; scoring_function::F = identity) where {F}
return iif(GPCM, theta, merge(beta, (; a = 1.0)); scoring_function)
end

function iif(M::Type{PCM}, theta, beta, y; scoring_function::F = identity) where {F}
return iif(GPCM, theta, merge(beta, (; a = 1.0)), y; scoring_function)
end

function iif(M::Type{RSM}, theta, beta; scoring_function::F = identity) where {F}
return iif(PCM, theta, beta; scoring_function)
end

function iif(M::Type{RSM}, theta, beta, y; scoring_function::F = identity) where {F}
return iif(PCM, theta, beta, y; scoring_function)
function iif(
M::Type{<:PolytomousItemResponseModel},
theta,
beta;
scoring_function::F = identity,
) where {F}
pars = merge_pars(GPCM, beta)
return iif(GPCM, theta, pars; scoring_function)
end

function iif(M::Type{GRSM}, theta, beta; scoring_function::F = identity) where {F}
return iif(GPCM, theta, beta; scoring_function)
end

function iif(M::Type{GRSM}, theta, beta, y; scoring_function::F = identity) where {F}
return iif(GPCM, theta, beta, y; scoring_function)
function iif(
M::Type{<:PolytomousItemResponseModel},
theta,
beta,
y;
scoring_function::F = identity,
) where {F}
prob = irf(M, theta, beta, y)
return prob * iif(M, theta, beta; scoring_function)
end
83 changes: 58 additions & 25 deletions src/irf.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ ability value `theta` given item parameters `beta`.
If `response_type(M) == Dichotomous`, then the item response function is evaluated for a
correct response (`y = 1`) by default.

## Models with polytomous responses
if `y` is omitted, then the item (category) response function for all categories is returend.

## Examples
### 1 Parameter Logistic Model
```jldoctest
Expand Down Expand Up @@ -86,42 +89,72 @@ julia> irf(RSM, 0.0, beta, 3)
```

"""
function irf(M::Type{OnePL}, theta::Real, beta::Real, y = 1)
prob = logistic(theta - beta)
function irf(M::Type{FourPL}, theta::Real, beta::NamedTuple, y = 1)
@unpack a, b, c, d = beta
prob = c + (d - c) * logistic(a * (theta - b))
return ifelse(y == 1, prob, 1 - prob)
end

function irf(M::Type{OnePL}, theta, beta, y = 1)
return irf(FourPL, theta, merge(beta, (a = 1.0, c = 0.0, d = 1.0)), y)
function irf(M::Type{<:DichotomousItemResponseModel}, theta, beta, y = 1)
pars = merge_pars(M, beta)
return irf(FourPL, theta, pars, y)
end

function irf(M::Type{TwoPL}, theta, beta, y = 1)
return irf(FourPL, theta, merge(beta, (; c = 0.0, d = 1.0)), y)
function irf(M::Type{OnePL}, theta::Real, beta::Real, y = 1)
prob = logistic(theta - beta)
return ifelse(y == 1, prob, 1 - prob)
end

function irf(M::Type{ThreePL}, theta, beta, y = 1)
return irf(FourPL, theta, merge(beta, (; d = 1.0)), y)
function irf(M::Type{GPCM}, theta, beta)
@unpack t = beta
probs = similar(t, length(t) + 1)
return irf!(M, probs, theta, beta)
end

function irf(M::Type{FourPL}, theta::Real, beta::NamedTuple, y = 1)
@unpack a, b, c, d = beta
prob = c + (d - c) * logistic(a * (theta - b))
return ifelse(y == 1, prob, 1 - prob)
function irf(M::Type{<:PolytomousItemResponseModel}, theta, beta)
pars = has_discrimination(M) ? beta : merge(beta, (; a = 1.0))
return irf(GPCM, theta, pars)
end

function irf(M::Type{GPCM}, theta, beta)
irf(M::Type{<:PolytomousItemResponseModel}, theta, beta, y) = irf(M, theta, beta)[y]

"""
$(SIGNATURES)

An in-place version of [`irf`](@ref) for polytomous item response models.
Provides efficient computation by mutating `probs` in-place, thus avoiding allocation of an
output vector.

## Examples
```jldoctest
julia> beta = (a = 1.2, b = 0.3, t = zeros(3));

julia> probs = zeros(length(beta.t) + 1);

julia> irf!(GPCM, probs, 0.0, beta)
4-element Vector{Float64}:
0.3961927292844976
0.2764142877832629
0.19284770477416754
0.13454527815807202

julia> probs
4-element Vector{Float64}:
0.3961927292844976
0.2764142877832629
0.19284770477416754
0.13454527815807202
```
"""
function irf!(M::Type{GPCM}, probs, theta, beta)
@unpack a, b, t = beta
extended = zeros(length(t) + 1)
@. extended[2:end] = a * (theta - b + t)
cumsum!(extended, extended)
softmax!(extended, extended)
return extended
probs[1] = 0.0
@. probs[2:end] = a * (theta - b + t)
cumsum!(probs, probs)
softmax!(probs, probs)
return probs
end

irf(M::Type{GPCM}, theta, beta, y) = irf(M, theta, beta)[y]
irf(M::Type{PCM}, theta, beta) = irf(GPCM, theta, merge(beta, (; a = 1.0)))
irf(M::Type{PCM}, theta, beta, y) = irf(GPCM, theta, merge(beta, (; a = 1.0)), y)
irf(M::Type{RSM}, theta, beta) = irf(PCM, theta, beta)
irf(M::Type{RSM}, theta, beta, y) = irf(PCM, theta, beta, y)
irf(M::Type{GRSM}, theta, beta) = irf(GPCM, theta, beta)
irf(M::Type{GRSM}, theta, beta, y) = irf(GPCM, theta, beta, y)
function irf!(M::Type{<:PolytomousItemResponseModel}, probs, theta, beta)
return irf!(GPCM, probs, theta, beta)
end
Loading
Loading