diff --git a/README.md b/README.md index a3c0bc2..fd51557 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/irt_ruby.rb b/lib/irt_ruby.rb index 967ff42..a7e273c 100644 --- a/lib/irt_ruby.rb +++ b/lib/irt_ruby.rb @@ -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 diff --git a/lib/irt_ruby/three_parameter_model.rb b/lib/irt_ruby/three_parameter_model.rb new file mode 100644 index 0000000..cb5b1f2 --- /dev/null +++ b/lib/irt_ruby/three_parameter_model.rb @@ -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 diff --git a/spec/irt_ruby/three_parameter_model_spec.rb b/spec/irt_ruby/three_parameter_model_spec.rb new file mode 100644 index 0000000..58cbd4c --- /dev/null +++ b/spec/irt_ruby/three_parameter_model_spec.rb @@ -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 diff --git a/spec/irt_ruby/two_parameter_model_spec.rb b/spec/irt_ruby/two_parameter_model_spec.rb index 0ef61e9..0ccd07a 100644 --- a/spec/irt_ruby/two_parameter_model_spec.rb +++ b/spec/irt_ruby/two_parameter_model_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" RSpec.describe IrtRuby::TwoParameterModel do