Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project are documented in this file.

## [Unreleased]

### Changed
- Clarified that response data must be a `Matrix` or array of arrays containing only integer `0`, integer `1`, or `nil`; floats, strings, booleans, and other values are rejected.

---

## [0.3.0] - 2025-01-14

### Changed
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ result = model.fit
puts "Abilities: #{result[:abilities]}"
puts "Difficulties: #{result[:difficulties]}"
```

Response data passed to model constructors must be either a `Matrix` or an
array of arrays. Each response value must be the integer `0`, the integer `1`,
or `nil` for missing data; floats such as `0.0`/`1.0`, strings, booleans, and
other values are rejected.

### Using 2PL and 3PL Models
```ruby
two_pl_model = IrtRuby::TwoParameterModel.new(data)
Expand Down
1 change: 1 addition & 0 deletions lib/irt_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "irt_ruby/version"
require "matrix"
require "irt_ruby/response_data_validator"
require "irt_ruby/rasch_model"
require "irt_ruby/two_parameter_model"
require "irt_ruby/three_parameter_model"
Expand Down
4 changes: 3 additions & 1 deletion lib/irt_ruby/rasch_model.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "irt_ruby/response_data_validator"

module IrtRuby
# A class representing the Rasch model for Item Response Theory (ability - difficulty).
# Incorporates:
Expand All @@ -20,7 +22,7 @@ def initialize(data,
# missing_strategy: :ignore (skip), :treat_as_incorrect, :treat_as_correct

@data = data
@data_array = data.to_a
@data_array = ResponseDataValidator.validate!(data)
num_rows = @data_array.size
num_cols = @data_array.first.size

Expand Down
59 changes: 59 additions & 0 deletions lib/irt_ruby/response_data_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module IrtRuby
# Validates response data accepted by IRT model constructors.
# @api private
module ResponseDataValidator
module_function

def validate!(data)
raise ArgumentError, "response data must be a Matrix or array of arrays" unless valid_data_container?(data)

data_array = data.to_a

raise ArgumentError, "response data must have at least one row" unless data_array.any?

validate_rows!(data_array)
validate_values!(data_array)

data_array
end

def validate_rows!(data_array)
first_row = data_array.first

raise ArgumentError, "response data must be a Matrix or array of arrays" unless first_row.is_a?(Array)

expected_columns = first_row.size
raise ArgumentError, "response data must have at least one column" if expected_columns.zero?

data_array.each_with_index do |row, index|
row_number = index + 1
raise ArgumentError, "response data row #{row_number} must be an Array" unless row.is_a?(Array)

next if row.size == expected_columns

raise ArgumentError, "response data must be rectangular; row #{row_number} has #{row.size} columns, expected #{expected_columns}"
end
end

def validate_values!(data_array)
data_array.each_with_index do |row, row_index|
row.each_with_index do |value, column_index|
next if valid_response?(value)

raise ArgumentError,
"response data contains invalid value #{value.inspect} at row #{row_index + 1}, column #{column_index + 1}; allowed values are 0, 1, and nil"
end
end
end

def valid_response?(value)
value.nil? || value.eql?(0) || value.eql?(1)
end

def valid_data_container?(data)
data.is_a?(Array) || (defined?(::Matrix) && data.is_a?(::Matrix))
end
end
end
4 changes: 3 additions & 1 deletion lib/irt_ruby/three_parameter_model.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "irt_ruby/response_data_validator"

module IrtRuby
# A class representing the Three-Parameter model (3PL) for Item Response Theory.
# Incorporates:
Expand All @@ -19,7 +21,7 @@ def initialize(data,
decay_factor: 0.5,
missing_strategy: :ignore)
@data = data
@data_array = data.to_a
@data_array = ResponseDataValidator.validate!(data)
num_rows = @data_array.size
num_cols = @data_array.first.size

Expand Down
4 changes: 3 additions & 1 deletion lib/irt_ruby/two_parameter_model.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "irt_ruby/response_data_validator"

module IrtRuby
# A class representing the Two-Parameter model (2PL) for IRT.
# Incorporates:
Expand All @@ -15,7 +17,7 @@ def initialize(data, max_iter: 1000, tolerance: 1e-6, param_tolerance: 1e-6,
learning_rate: 0.01, decay_factor: 0.5,
missing_strategy: :ignore)
@data = data
@data_array = data.to_a
@data_array = ResponseDataValidator.validate!(data)
num_rows = @data_array.size
num_cols = @data_array.first.size

Expand Down
2 changes: 2 additions & 0 deletions spec/irt_ruby/rasch_model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require "spec_helper"

RSpec.describe IrtRuby::RaschModel do
it_behaves_like "response data validation"

let(:data_array) do
[
[1, 1, 0],
Expand Down
2 changes: 2 additions & 0 deletions spec/irt_ruby/three_parameter_model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require "spec_helper"

RSpec.describe IrtRuby::ThreeParameterModel do
it_behaves_like "response data validation"

let(:data_array) do
[
[1, 1, 0],
Expand Down
2 changes: 2 additions & 0 deletions spec/irt_ruby/two_parameter_model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require "spec_helper"

RSpec.describe IrtRuby::TwoParameterModel do
it_behaves_like "response data validation"

let(:data_array) do
[
[1, 1, 0],
Expand Down
39 changes: 39 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,45 @@

require "irt_ruby"

RSpec.shared_examples "response data validation" do
it "rejects empty data" do
expect { described_class.new([]) }.to raise_error(ArgumentError, /at least one row/)
end

it "rejects empty rows" do
expect { described_class.new([[]]) }.to raise_error(ArgumentError, /at least one column/)
end

it "rejects ragged rows" do
expect { described_class.new([[1, 0], [1]]) }.to raise_error(ArgumentError, /rectangular; row 2/)
end

it "rejects invalid response values" do
expect { described_class.new([[1, 2], [0, nil]]) }.to raise_error(ArgumentError, /invalid value 2/)
end

it "rejects float response values that compare equal to allowed integers" do
expect { described_class.new([[0.0]]) }.to raise_error(ArgumentError, /invalid value 0\.0/)
expect { described_class.new([[1.0]]) }.to raise_error(ArgumentError, /invalid value 1\.0/)
end

it "rejects string response values" do
expect { described_class.new([["1"]]) }.to raise_error(ArgumentError, /invalid value "1"/)
end

it "rejects false response values" do
expect { described_class.new([[false]]) }.to raise_error(ArgumentError, /invalid value false/)
end

it "rejects true response values" do
expect { described_class.new([[true]]) }.to raise_error(ArgumentError, /invalid value true/)
end

it "rejects hash input even when it can be converted to an array" do
expect { described_class.new({ [0] => 1 }) }.to raise_error(ArgumentError, /Matrix or array of arrays/)
end
end

RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
Expand Down
Loading