Skip to content

Commit

Permalink
Add Three-Parameter IRT Model and Corresponding Tests (#4)
Browse files Browse the repository at this point in the history
- Implement Three-Parameter Model (3PL) for Item Response Theory
- Add tests for Three-Parameter Model
- Update main library file to include the new model
- Adjust README for the new model usage
  • Loading branch information
kholdrex authored Jun 9, 2024
1 parent 212ec5b commit 22b0d94
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

# IrtRuby

IrtRuby is a Ruby gem that provides implementations of the Rasch model and the Two-Parameter model for Item Response Theory (IRT). It allows you to estimate the abilities of individuals and the difficulties of items based on their responses to a set of items.
IrtRuby is a Ruby gem that provides implementations of the Rasch model, the Two-Parameter model, and the Three-Parameter model for Item Response Theory (IRT). It allows you to estimate the abilities of individuals and the difficulties of items based on their responses to a set of items.

## Installation

Expand Down
1 change: 1 addition & 0 deletions lib/irt_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "irt_ruby/version"
require "irt_ruby/rasch_model"
require "irt_ruby/two_parameter_model"
require "irt_ruby/three_parameter_model"

module IrtRuby
class Error < StandardError; end
Expand Down
71 changes: 71 additions & 0 deletions lib/irt_ruby/three_parameter_model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

require "matrix"

module IrtRuby
# A class representing the Three-Parameter model for Item Response Theory.
class ThreeParameterModel
def initialize(data)
@data = data
@abilities = Array.new(data.row_count) { rand }
@difficulties = Array.new(data.column_count) { rand }
@discriminations = Array.new(data.column_count) { rand }
@guessings = Array.new(data.column_count) { rand * 0.3 }
@max_iter = 1000
@tolerance = 1e-6
end

def sigmoid(x)
1.0 / (1.0 + Math.exp(-x))
end

def probability(theta, a, b, c)
c + (1 - c) * sigmoid(a * (theta - b))
end

def likelihood
likelihood = 0
@data.row_vectors.each_with_index do |row, i|
row.to_a.each_with_index do |response, j|
prob = probability(@abilities[i], @discriminations[j], @difficulties[j], @guessings[j])
if response == 1
likelihood += Math.log(prob)
elsif response.zero?
likelihood += Math.log(1 - prob)
end
end
end
likelihood
end

def update_parameters
last_likelihood = likelihood
@max_iter.times do |_iter|
@data.row_vectors.each_with_index do |row, i|
row.to_a.each_with_index do |response, j|
prob = probability(@abilities[i], @discriminations[j], @difficulties[j], @guessings[j])
error = response - prob
@abilities[i] += 0.01 * error * @discriminations[j]
@difficulties[j] -= 0.01 * error * @discriminations[j]
@discriminations[j] += 0.01 * error * (@abilities[i] - @difficulties[j])
@guessings[j] += 0.01 * error * (1 - prob)
end
end
current_likelihood = likelihood
break if (last_likelihood - current_likelihood).abs < @tolerance

last_likelihood = current_likelihood
end
end

def fit
update_parameters
{
abilities: @abilities,
difficulties: @difficulties,
discriminations: @discriminations,
guessings: @guessings
}
end
end
end
42 changes: 42 additions & 0 deletions spec/irt_ruby/three_parameter_model_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe IrtRuby::ThreeParameterModel do
let(:data) { Matrix[[1, 0, 1], [0, 1, 0], [1, 1, 1]] }
let(:model) { IrtRuby::ThreeParameterModel.new(data) }

describe "#initialize" do
it "initializes with data" do
expect(model.instance_variable_get(:@data)).to eq(data)
end
end

describe "#sigmoid" do
it "calculates the sigmoid function" do
expect(model.sigmoid(0)).to eq(0.5)
end
end

describe "#probability" do
it "calculates the probability with guessing parameter" do
expect(model.probability(0, 1, 0, 0.2)).to be_within(0.01).of(0.6)
end
end

describe "#likelihood" do
it "calculates the likelihood of the data" do
expect(model.likelihood).to be_a(Float)
end
end

describe "#fit" do
it "fits the model and returns abilities, difficulties, discriminations, and guessings" do
result = model.fit
expect(result[:abilities].size).to eq(data.row_count)
expect(result[:difficulties].size).to eq(data.column_count)
expect(result[:discriminations].size).to eq(data.column_count)
expect(result[:guessings].size).to eq(data.column_count)
end
end
end
2 changes: 2 additions & 0 deletions spec/irt_ruby/two_parameter_model_spec.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe IrtRuby::TwoParameterModel do
Expand Down

0 comments on commit 22b0d94

Please sign in to comment.