Java tutorial
/* * Copyright 2012 J. Patrick Meyer * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.itemanalysis.psychometrics.irt.model; import com.itemanalysis.psychometrics.irt.estimation.ItemParamPrior; import org.apache.commons.math3.stat.descriptive.moment.Mean; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; import java.util.Arrays; /** * This version of the Generalized Partial Credit Model (GPCM) uses a discrimination * parameter, a difficulty parameter (b), and one or more threshold parameters. For an * item with M categories, there are M-1 threshold parameters (t). For k = 2,..., M, * Let Zk = sum_{v=2}^k {D*a*(theta-b+t_v)} if k>1 and Zk = 0 if k==1. The probability * of a response of k is given by, exp(Zk)/(1+sum_{k=2}^M {exp(Zk)}). * * This form of the GPCM is used in PARSCALE. * * The relationship with IrmGPCM is that the thresholds and difficulty in IrmGPCM2 * can be combined to get teh step parameters in IrmGPCM such that, b_v = (b - t_v). * This decomposition of the step parameters is given in Muraki's 1992 article. It * differs from the decomposition of the step parameters in Linacre's parameterization * of the PCM as used in jMetrik and WINSTEPS. Linacre's parameterization is found * in IrmPCM. * * TODO currently stores m-1 thresholds for an item with m categories. Will need to change to an array of m thresholds when use this model for estimation. * */ public class IrmGPCM2 extends AbstractItemResponseModel { private double discrimination = 1.0; private double proposalDiscrimination = 1.0; private double discriminationStdError = 0.0; private double difficulty = 0.0; private double proposalDifficulty = 0.0; private double difficultyStdError = 0.0; private double D = 1.7; private double[] threshold; private double[] proposalThreshold; private double[] thresholdStdError; private ItemParamPrior discriminationPrior = null; private ItemParamPrior difficultyPrior = null; private ItemParamPrior[] stepPrior = null; /** * Default constructor * * @param discrimination item discrimination parameter * @param difficulty item difficulty parameter * @param threshold an array of m-1 threshold parameters for an m category item. * @param D a scaling constant that is either 1.0, 1.7, or 1.712. */ public IrmGPCM2(double discrimination, double difficulty, double[] threshold, double D) { this.discrimination = discrimination; this.difficulty = difficulty; this.threshold = threshold; this.thresholdStdError = new double[threshold.length]; this.D = D; ncatM1 = threshold.length; ncat = ncatM1 + 1; maxCategory = ncat; defaultScoreWeights(); } public double probability(double theta, double[] iparam, int category, double D) { double t = numer(theta, iparam, category, D); double b = denom(theta, iparam, D); return t / b; } /** * Computes probability of a response using parameters stored in the object. * * @param theta person proficiency value * @param category category for which the probability of a response is sought. * @return probability of responding in category */ public double probability(double theta, int category) { double t = numer(theta, category); double b = denom(theta); return t / b; } private double numer(double theta, double[] iparam, int category, double D) { double Zk = 0; double a = iparam[0]; double b = iparam[1]; double[] t = Arrays.copyOfRange(iparam, 2, iparam.length); //first category Zk = D * a * (theta - b); for (int k = 0; k < category; k++) { Zk += D * a * (theta - b + t[k]); } return Math.exp(Zk); } /** * Computes the expression for responding in a category. * It is the numerator for the probability of observing a response. * * @param theta person proficiency value * @param category category for which probability is sought * @return expression for responding in category */ private double numer(double theta, int category) { double Zk = 0; //first category Zk = D * discrimination * (theta - difficulty); for (int k = 0; k < category; k++) { Zk += D * discrimination * (theta - difficulty + threshold[k]); } return Math.exp(Zk); } private double denom(double theta, double[] iparam, double D) { double denom = 0.0; double expZk = 0.0; for (int k = 0; k < ncat; k++) { expZk = numer(theta, iparam, k, D); denom += expZk; } return denom; } /** * Denominator is the sum of the numerators. This method is used for * computing the probability of a response. * * @param theta * @return */ private double denom(double theta) { double denom = 0.0; double expZk = 0.0; for (int k = 0; k < ncat; k++) { expZk = numer(theta, k); denom += expZk; } return denom; } /** * Partial derivative with respect to theta. * * @param theta person proficiency value * @return partial derivative at theta */ public double derivTheta(double theta) { double d1 = denom(theta); double d2 = d1 * d1; double x1 = subCalcForDerivTheta(theta); double n1 = 0.0; double deriv = 0.0; double p1 = 0.0; double p2 = 0.0; for (int k = 0; k < ncat; k++) { n1 = numer(theta, k); p1 = (D * n1 * (1.0 + k) * discrimination) / d1; p2 = (n1 * x1) / d2; deriv += scoreWeight[k] * (p1 - p2); } return deriv; } /** * Calculation needed for derivTheta(). * * @param theta person proficiency value * @return */ private double subCalcForDerivTheta(double theta) { double sum = 0.0; for (int k = 0; k < ncat; k++) { sum += D * numer(theta, k) * (1.0 + k) * discrimination; } return sum; } /** * computes the expected value using parameters stored in the object * * @param theta * @return */ public double expectedValue(double theta) { double ev = 0; for (int i = 0; i < ncat; i++) { ev += scoreWeight[i] * probability(theta, i); } return ev; } public double[] nonZeroPrior(double[] param) { double[] p = Arrays.copyOf(param, param.length); if (discriminationPrior != null) p[0] = discriminationPrior.nearestNonZero(param[0]); if (difficultyPrior != null) p[1] = difficultyPrior.nearestNonZero(param[1]); for (int k = 2; k < param.length; k++) { if (stepPrior[k - 2] != null) p[k] = stepPrior[k - 2].nearestNonZero(param[k]); } return p; } public void setDiscriminationPrior(ItemParamPrior discriminationPrior) { this.discriminationPrior = discriminationPrior; } public void setDifficultyPrior(ItemParamPrior difficultyPrior) { this.difficultyPrior = difficultyPrior; } public void setGuessingPrior(ItemParamPrior guessingPrior) { } public void setSlippingPrior(ItemParamPrior slippingPrior) { } public void setStepPriorAt(ItemParamPrior prior, int index) { this.stepPrior[index] = prior; } public double[] gradient(double theta, double[] iparam, int k, double D) { //empty method return null; } public double[] gradient(double theta, int k) { //empty method return null; } public double addPriorsToLogLikelihood(double ll, double[] iparam) { return ll; } public double[] addPriorsToLogLikelihoodGradient(double[] loglikegrad, double[] iparam) { //empty method return loglikegrad; } public double itemInformationAt(double theta) { double T = 0; double prob = 0.0; double sum1 = 0.0; double sum2 = 0.0; double a2 = discrimination * discrimination; for (int i = 0; i < ncat; i++) { prob = probability(theta, i); T = scoreWeight[i]; sum1 += T * T * prob; sum2 += T * prob; } double info = D * D * a2 * (sum1 - Math.pow(sum2, 2)); return info; } public void incrementMeanSigma(Mean mean, StandardDeviation sd) { for (int i = 0; i < ncatM1; i++) { mean.increment(difficulty - threshold[i]); sd.increment(difficulty - threshold[i]); } } public void incrementMeanMean(Mean meanDiscrimination, Mean meanDifficulty) { meanDiscrimination.increment(discrimination); for (int i = 0; i < ncatM1; i++) { meanDifficulty.increment(difficulty - threshold[i]); } } public void scale(double intercept, double slope) { discrimination /= slope; discriminationStdError *= slope; difficulty = difficulty * slope + intercept; difficultyStdError *= slope; for (int i = 0; i < ncatM1; i++) { threshold[i] = threshold[i] * slope; thresholdStdError[i] = thresholdStdError[i] * slope; } } public int getNumberOfParameters() { return threshold.length + 2; } public int getNumberOfEstimatedParameters() { //TODO change to threshold.length+1 when threshold array changed to include first threshold that is fixed to zero. if (isFixed) return 0; return threshold.length + 2; } public double getScalingConstant() { return D; } /** * Returns the probability of a response with a linear transformatin of the parameters. * This transformation is such that Form X (New Form) is transformed to the scale of Form Y * (Old Form). It implements the backwards (New to Old) transformation as described in Kim * and Kolen. * * @param theta examinee proficiency parameter * @param category item response * @param intercept intercept coefficient of linear transformation * @param slope slope (i.e. scale) parameter of the linear transformation * @return probability of a response at values of linearly transformed item parameters */ public double tStarProbability(double theta, int category, double intercept, double slope) { if (category > maxCategory || category < minCategory) return 0; double[] iparam = new double[getNumberOfParameters()]; iparam[0] = discrimination / slope; iparam[1] = difficulty * slope + intercept; for (int i = 0; i < threshold.length; i++) { iparam[i + 2] = threshold[i] * slope; } return probability(theta, iparam, category, D); // double Zk = 0; // double expZk = 0; // double numer = 0; // double denom = 0; // double a = discrimination/slope; // double b = 0; // double t = 0; // for(int k=0;k<ncat;k++){ // Zk = 0; // for(int v=1;v<(k+1);v++){ // b = difficulty*slope+intercept; // t = threshold[v-1]*slope; // Zk += D*a*(theta-(b-t)); // } // expZk = Math.exp(Zk); // if(k==category) numer = expZk; // denom += expZk; // } // return numer/denom; } /** * computes the expected value using parameters stored in the object * * @param theta * @return */ public double tStarExpectedValue(double theta, double intercept, double slope) { double ev = 0; for (int i = 0; i < ncat; i++) { ev += scoreWeight[i] * tStarProbability(theta, i, intercept, slope); } return ev; } public double tSharpProbability(double theta, int category, double intercept, double slope) { if (category > maxCategory || category < minCategory) return 0; double[] iparam = new double[getNumberOfParameters()]; iparam[0] = discrimination * slope; iparam[1] = (difficulty - intercept) / slope; for (int i = 0; i < threshold.length; i++) { iparam[i + 2] = threshold[i] / slope; } return probability(theta, iparam, category, D); // double Zk = 0; // double expZk = 0; // double numer = 0; // double denom = 0; // double a = discrimination*slope; // double b = 0; // double t = 0; // // for(int k=0;k<ncat;k++){ // Zk = 0; // for(int v=1;v<(k+1);v++){ // b = (difficulty-intercept)/slope; // t = threshold[v-1]/slope; // Zk += D*a*(theta-(b-t)); // } // expZk = Math.exp(Zk); // if(k==category) numer = expZk; // denom += expZk; // } // return numer/denom; } public double tSharpExpectedValue(double theta, double intercept, double slope) { double ev = 0; for (int i = 0; i < ncat; i++) { ev += scoreWeight[i] * tSharpProbability(theta, i, intercept, slope); } return ev; } public String toString() { String s = "[" + getDiscrimination() + ", " + getDifficulty(); double[] sp = getStepParameters(); for (int i = 0; i < sp.length; i++) { s += ", " + sp[i]; } s += "]"; return s; } public IrmType getType() { return IrmType.GPCM2; } //=====================================================================================================================// // GETTER AND SETTER METHODS MAINLY FOR USE WHEN ESTIMATING PARAMETERS // //=====================================================================================================================// public double[] getItemParameterArray() { double[] ip = new double[getNumberOfParameters()]; ip[0] = discrimination; ip[1] = difficulty; for (int k = 0; k < ncatM1; k++) { ip[k + 2] = threshold[k]; } return ip; } public void setStandardErrors(double[] x) { discriminationStdError = x[0]; difficultyStdError = x[1]; for (int k = 0; k < ncatM1; k++) { thresholdStdError[k] = x[k + 2]; } } public double getDifficulty() { return difficulty; } public void setDifficulty(double difficulty) { this.difficulty = difficulty; } public double getProposalDifficulty() { return proposalDifficulty; } public void setProposalDifficulty(double difficulty) { this.proposalDifficulty = difficulty; } public double getDifficultyStdError() { return difficultyStdError; } public void setDifficultyStdError(double stdError) { difficultyStdError = stdError; } public double getDiscrimination() { return discrimination; } public void setDiscrimination(double discrimination) { this.discrimination = discrimination; } public void setProposalDiscrimination(double discrimination) { this.proposalDiscrimination = discrimination; } public double getDiscriminationStdError() { return discriminationStdError; } public void setDiscriminationStdError(double stdError) { discriminationStdError = stdError; } public double getGuessing() { return 0.0; } public void setGuessing(double guessing) { } public void setProposalGuessing(double guessing) { } public double getGuessingStdError() { return Double.NaN; } public void setGuessingStdError(double stdError) { } public void setSlipping(double slipping) { } public void setProposalSlipping(double slipping) { } public void setSlippingStdError(double slipping) { } public double getSlipping() { return Double.NaN; } public double getSlippingStdError() { return Double.NaN; } public double[] getThresholdParameters() { return threshold; } public void setThresholdParameters(double[] thresholds) { this.threshold = thresholds; } public void setProposalThresholds(double[] thresholds) { this.proposalThreshold = threshold; } public double[] getThresholdStdError() { return thresholdStdError; } public void setThresholdStdError(double[] stdError) { thresholdStdError = stdError; } public double[] getStepParameters() { double[] t = new double[ncatM1]; for (int k = 0; k < ncatM1; k++) { t[k] = difficulty - threshold[k]; } return t; } public void setStepParameters(double[] step) { } public void setProposalStepParameters(double[] step) { } public double[] getStepStdError() { double[] sp = new double[ncat]; for (int k = 0; k < ncat; k++) { sp[k] = Double.NaN; } return sp; } public void setStepStdError(double[] stdError) { } public double acceptAllProposalValues() { double max = 0; if (!isFixed) { double delta = Math.abs(this.difficulty - this.proposalDifficulty); if (proposalDifficulty >= 1) delta /= proposalDifficulty; max = Math.max(max, delta); delta = Math.abs(this.discrimination - this.proposalDiscrimination); if (proposalDiscrimination >= 1) delta /= proposalDiscrimination; max = Math.max(max, delta); for (int m = 0; m < getNcat(); m++) { delta = Math.abs(this.threshold[m] - this.proposalThreshold[m]); if (proposalThreshold[m] >= 1) delta /= proposalThreshold[m]; max = Math.max(max, delta); } this.difficulty = this.proposalDifficulty; this.discrimination = this.proposalDiscrimination; this.threshold = this.proposalThreshold; } return max; } //=====================================================================================================================// // END GETTER AND SETTER METHODS // //=====================================================================================================================// }