From 8e86eac94c6d667083f33d14bb755d1a31af6362 Mon Sep 17 00:00:00 2001 From: Alex Kholodniak Date: Sat, 8 Jun 2024 21:55:26 -0500 Subject: [PATCH] Add Two-Parameter IRT Model and Corresponding Tests - Implement Two-Parameter Model (2PL) for Item Response Theory - Add tests for Two-Parameter Model - Update main library file to include the new model - Adjust README for the new model usage --- README.md | 2 +- lib/irt_ruby.rb | 1 + lib/irt_ruby/two_parameter_model.rb | 60 +++++++++++++++++++++++ spec/irt_ruby/rasch_model_spec.rb | 2 +- spec/irt_ruby/two_parameter_model_spec.rb | 33 +++++++++++++ 5 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 lib/irt_ruby/two_parameter_model.rb create mode 100644 spec/irt_ruby/two_parameter_model_spec.rb diff --git a/README.md b/README.md index 42ed306..a3c0bc2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # IrtRuby -IrtRuby is a Ruby gem that provides a simple implementation of the Rasch 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 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. ## Installation diff --git a/lib/irt_ruby.rb b/lib/irt_ruby.rb index 565e18c..967ff42 100644 --- a/lib/irt_ruby.rb +++ b/lib/irt_ruby.rb @@ -2,6 +2,7 @@ require "irt_ruby/version" require "irt_ruby/rasch_model" +require "irt_ruby/two_parameter_model" module IrtRuby class Error < StandardError; end diff --git a/lib/irt_ruby/two_parameter_model.rb b/lib/irt_ruby/two_parameter_model.rb new file mode 100644 index 0000000..6d947b4 --- /dev/null +++ b/lib/irt_ruby/two_parameter_model.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "matrix" + +module IrtRuby + # A class representing the Two-Parameter model for Item Response Theory. + class TwoParameterModel + 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 } + @max_iter = 1000 + @tolerance = 1e-6 + end + + def sigmoid(x) + 1.0 / (1.0 + Math.exp(-x)) + end + + def likelihood + likelihood = 0 + @data.row_vectors.each_with_index do |row, i| + row.to_a.each_with_index do |response, j| + prob = sigmoid(@discriminations[j] * (@abilities[i] - @difficulties[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 = sigmoid(@discriminations[j] * (@abilities[i] - @difficulties[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]) + 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 } + end + end +end diff --git a/spec/irt_ruby/rasch_model_spec.rb b/spec/irt_ruby/rasch_model_spec.rb index d49f8bc..375f7ec 100644 --- a/spec/irt_ruby/rasch_model_spec.rb +++ b/spec/irt_ruby/rasch_model_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "irt_ruby" +require "spec_helper" RSpec.describe IrtRuby::RaschModel do let(:data) { Matrix[[1, 0, 1], [0, 1, 0], [1, 1, 1]] } diff --git a/spec/irt_ruby/two_parameter_model_spec.rb b/spec/irt_ruby/two_parameter_model_spec.rb new file mode 100644 index 0000000..0ef61e9 --- /dev/null +++ b/spec/irt_ruby/two_parameter_model_spec.rb @@ -0,0 +1,33 @@ +require "spec_helper" + +RSpec.describe IrtRuby::TwoParameterModel do + let(:data) { Matrix[[1, 0, 1], [0, 1, 0], [1, 1, 1]] } + let(:model) { IrtRuby::TwoParameterModel.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 "#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, and discriminations" 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) + end + end +end