def onepl_mml(dataset, alpha=None, options=None): """ Estimates parameters in an 1PL IRT Model. Args: dataset: [items x participants] matrix of True/False Values alpha: [int] discrimination constraint options: dictionary with updates to default options Returns: discrimination: (float) estimate of test discrimination difficulty: (1d array) estimates of item diffiulties Options: * distribution: callable * quadrature_bounds: (float, float) * quadrature_n: int """ options = validate_estimation_options(options) quad_start, quad_stop = options['quadrature_bounds'] quad_n = options['quadrature_n'] # Difficulty Estimation parameters n_items = dataset.shape[0] n_no, n_yes = get_true_false_counts(dataset) scalar = n_yes / (n_yes + n_no) unique_sets, counts = np.unique(dataset, axis=1, return_counts=True) the_sign = convert_responses_to_kernel_sign(unique_sets) discrimination = np.ones((n_items,)) difficulty = np.zeros((n_items,)) # Quadrature Locations theta = _get_quadrature_points(quad_n, quad_start, quad_stop) distribution = options['distribution'](theta) # Inline definition of cost function to minimize def min_func(estimate): discrimination[:] = estimate _mml_abstract(difficulty, scalar, discrimination, theta, distribution, options) partial_int = _compute_partial_integral(theta, difficulty, discrimination, the_sign) # add distribution partial_int *= distribution otpt = integrate.fixed_quad( lambda x: partial_int, quad_start, quad_stop, n=quad_n)[0] return -np.log(otpt).dot(counts) # Perform the minimization if alpha is None: # OnePL Method alpha = fminbound(min_func, 0.25, 10) else: # Rasch Method min_func(alpha) return alpha, difficulty
def test_get_true_false_counts(self): """Testing the counting of true and false.""" test_array = np.array([[1., 0., 4.3, 2.1, np.nan], [np.nan, np.nan, -1, 3.2, 1.2], [0.0, 1.0, 1.0, 0.0, 1.0]]) n_false, n_true = get_true_false_counts(test_array) expected_true = [1, 0, 3] expected_false = [1, 0, 2] np.testing.assert_array_equal(expected_true, n_true) np.testing.assert_array_equal(expected_false, n_false)
def threepl_mml(dataset, options=None): """ Estimates parameters in a 3PL IRT model. Args: dataset: [items x participants] matrix of True/False Values options: dictionary with updates to default options Returns: discrimination: (1d array) estimate of item discriminations difficulty: (1d array) estimates of item diffiulties guessing: (1d array) estimates of item guessing Options: * max_iteration: int * distribution: callable * quadrature_bounds: (float, float) * quadrature_n: int """ options = validate_estimation_options(options) quad_start, quad_stop = options['quadrature_bounds'] quad_n = options['quadrature_n'] n_items = dataset.shape[0] n_no, n_yes = get_true_false_counts(dataset) scalar = n_yes / (n_yes + n_no) unique_sets, counts = np.unique(dataset, axis=1, return_counts=True) the_sign = convert_responses_to_kernel_sign(unique_sets) theta, weights = _get_quadrature_points(quad_n, quad_start, quad_stop) distribution = options['distribution'](theta) distribution_x_weights = distribution * weights # Perform the minimization discrimination = np.ones((n_items,)) difficulty = np.zeros((n_items,)) guessing = np.zeros((n_items,)) local_scalar = np.zeros((1, 1)) for iteration in range(options['max_iteration']): previous_discrimination = discrimination.copy() # Quadrature evaluation for values that do not change # This is done during the outer loop to address rounding errors partial_int = _compute_partial_integral_3pl(theta, difficulty, discrimination, guessing, the_sign) partial_int *= distribution for ndx in range(n_items): # pylint: disable=cell-var-from-loop # remove contribution from current item local_int = _compute_partial_integral_3pl(theta, difficulty[ndx, None], discrimination[ndx, None], guessing[ndx, None], the_sign[ndx, None]) partial_int /= local_int def min_func_local(estimate): discrimination[ndx] = estimate[0] guessing[ndx] = estimate[1] local_scalar[0, 0] = (scalar[ndx] - guessing[ndx]) / (1. - guessing[ndx]) _mml_abstract(difficulty[ndx, None], local_scalar, discrimination[ndx, None], theta, distribution_x_weights) estimate_int = _compute_partial_integral_3pl(theta, difficulty[ndx, None], discrimination[ndx, None], guessing[ndx, None], the_sign[ndx, None]) estimate_int *= partial_int otpt = integrate.fixed_quad( lambda x: estimate_int, quad_start, quad_stop, n=quad_n)[0] return -np.log(otpt).dot(counts) # Solve for the discrimination parameters initial_guess = [discrimination[ndx], guessing[ndx]] fmin_slsqp(min_func_local, initial_guess, bounds=([0.25, 4], [0, .33]), iprint=False) # Update the partial integral based on the new found values estimate_int = _compute_partial_integral_3pl(theta, difficulty[ndx, None], discrimination[ndx, None], guessing[ndx, None], the_sign[ndx, None]) # update partial integral partial_int *= estimate_int if np.abs(discrimination - previous_discrimination).max() < 1e-3: break return {'Discrimination': discrimination, 'Difficulty': difficulty, 'Guessing': guessing}
def onepl_mml(dataset, alpha=None, options=None): """ Estimates parameters in an 1PL IRT Model. Args: dataset: [items x participants] matrix of True/False Values alpha: [int] discrimination constraint options: dictionary with updates to default options Returns: discrimination: (float) estimate of test discrimination difficulty: (1d array) estimates of item diffiulties Options: * distribution: callable * quadrature_bounds: (float, float) * quadrature_n: int """ options = validate_estimation_options(options) quad_start, quad_stop = options['quadrature_bounds'] quad_n = options['quadrature_n'] # Difficulty Estimation parameters n_items = dataset.shape[0] n_no, n_yes = get_true_false_counts(dataset) scalar = n_yes / (n_yes + n_no) unique_sets, counts = np.unique(dataset, axis=1, return_counts=True) invalid_response_mask = unique_sets == INVALID_RESPONSE unique_sets[invalid_response_mask] = 0 # For Indexing, fixed later discrimination = np.ones((n_items, )) difficulty = np.zeros((n_items, )) # Quadrature Locations theta, weights = _get_quadrature_points(quad_n, quad_start, quad_stop) distribution = options['distribution'](theta) distribution_x_weights = distribution * weights # Inline definition of cost function to minimize def min_func(estimate): discrimination[:] = estimate _mml_abstract(difficulty, scalar, discrimination, theta, distribution_x_weights) partial_int = np.ones((unique_sets.shape[1], theta.size)) for ndx in range(n_items): partial_int *= _compute_partial_integral( theta, difficulty[ndx], discrimination[ndx], unique_sets[ndx], invalid_response_mask[ndx]) partial_int *= distribution_x_weights # compute_integral otpt = np.sum(partial_int, axis=1) return -np.log(otpt).dot(counts) # Perform the minimization if alpha is None: # OnePL Method alpha = fminbound(min_func, 0.25, 10) else: # Rasch Method min_func(alpha) return {"Discrimination": alpha, "Difficulty": difficulty}