class Kot::HillClimbingEstimator

Public Instance Methods

dodd(est_theta: 0.0, items: [], last_response: []) click to toggle source

Estimates theta when all responses are true or all are false, based on Dodd, 1990. “The variable stepsize changed the 0 estimate by half the distance to the appropriate … value in the item pool.”

# File lib/kot/hill_climbing_estimator.rb, line 6
def dodd(est_theta: 0.0, items: [], last_response: [])
  max_b = items.map(&:b).max
  min_b = items.map(&:b).min

  last_response ? est_theta + ((max_b - est_theta) / 2.0) : est_theta - ((est_theta - min_b) / 2.0)
end
estimate(responses: [], items: [], all_items: [], est_theta: 0.0) click to toggle source

Estimate theta using multiple iterations of a hill climb, falling back to {#dodd} if all responses are true or false.

# File lib/kot/hill_climbing_estimator.rb, line 47
def estimate(responses: [], items: [], all_items: [], est_theta: 0.0)
  if responses.uniq.count == 1
    raise ArgumentError, "Responses are all #{responses.first} but missing all_items argument" if all_items.empty?
    return dodd(est_theta: est_theta, items: all_items, last_response: responses.last)
  end

  # TODO: More intelligently decide limits to theta
  lower_bound = (items.map(&:b) << -3.0).min
  upper_bound = (items.map(&:b) << +3.0).max

  return lower_bound if upper_bound == lower_bound

  best_theta = - Float::INFINITY

  10.times do
    max_ll = - Float::INFINITY

    best_theta, max_ll, lower_bound, upper_bound = 
      estimate_iteration(best_theta, max_ll, lower_bound, upper_bound, responses, items)

    break if lower_bound == upper_bound
  end

  best_theta
end
estimate_iteration(best_theta, max_ll, lower_bound, upper_bound, responses, items) click to toggle source

Performs a single iteration of the hill climb, starting from one bound towards the other.

# File lib/kot/hill_climbing_estimator.rb, line 14
def estimate_iteration(best_theta, max_ll, lower_bound, upper_bound, responses, items)
  step_size = (upper_bound - lower_bound) / 10

  case step_size <=> 0
  when 1
    intervals = (lower_bound..upper_bound).step(step_size).each
  when -1
    intervals = (upper_bound..lower_bound).step(step_size.abs).reverse_each
  when 0
    intervals = []
  end

  intervals.each do |ii|

    ll = ItemResponseTheory.log_likelihood(ii, responses, items)

    if ll > max_ll
      max_ll = ll

      #TODO: precision-based early exit
      best_theta = ii
    else
      lower_bound = best_theta - step_size
      upper_bound = ii
      break
    end

  end

  [best_theta, max_ll, lower_bound, upper_bound]
end