Java tutorial
/** * Copyright 2014 J. Patrick Meyer * <p/> * 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 * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * 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; import java.util.Formatter; public class Irm4PL extends AbstractItemResponseModel { private int numberOfParameters = 4; private double discrimination = 1; private double difficulty = 0; private double guessing = 0; private double slipping = 0; private double D = 1.7; private double proposalDiscrimination = 1; private double proposalDifficulty = 0; private double proposalGuessing = 0; private double proposalSlipping = 0; private double discriminationStdError = 1; private double difficultyStdError = 0; private double guessingStdError = 0; private double slippingStdError = 0; private ItemParamPrior discriminationPrior = null; private ItemParamPrior difficultyPrior = null; private ItemParamPrior guessingPrior = null; private ItemParamPrior slippingPrior = null; /** * Constructor for three parameter logistic model * * @param discrimination item discrimination parameter * @param difficulty item difficulty parameter * @param guessing lower-asymptote parameter * @param slipping upper-asymptote parameter * @param D scaling factor */ public Irm4PL(double discrimination, double difficulty, double guessing, double slipping, double D) { this.discrimination = discrimination; this.difficulty = difficulty; this.guessing = guessing; this.slipping = slipping; this.proposalDiscrimination = discrimination; this.proposalDifficulty = difficulty; this.proposalGuessing = guessing; this.proposalSlipping = slipping; this.D = D; this.numberOfParameters = 4; this.ncat = 2; defaultScoreWeights(); } public double probability(double theta, double[] iparam, int response, double D) { if (response == 1) { return probRight(theta, iparam, D); } else { return probWrong(theta, iparam, D); } } public double probability(double theta, int response) { double prob = 0.0; if (response == 1) { prob = probRight(theta); } else { prob = probWrong(theta); } return Math.min(1.0, Math.max(0.0, prob)); //always return value between 0 and 1 } private double probRight(double theta, double[] iparam, double D) { if (iparam[0] < 0) return 0; double z = Math.exp(D * iparam[0] * (theta - iparam[1])); return iparam[2] + (iparam[3] - iparam[2]) * z / (1 + z); } private double probWrong(double theta, double[] iparam, double D) { if (iparam[0] < 0) return 0; return 1.0 - probRight(theta, iparam, D); } private double probRight(double theta) { if (guessing < 0) return 0; double top = Math.exp(D * discrimination * (theta - difficulty)); double prob = guessing + (slipping - guessing) * top / (1 + top); return prob; } private double probWrong(double theta) { if (guessing < 0) return 0; return 1.0 - probRight(theta); } public double expectedValue(double theta) { return scoreWeight[1] * probRight(theta); } /** * Computes the gradientAt (vector of first partial derivatives) with respect to the item parameters. * This method uses item parameters passed to teh method. It does NOT use item parameters stored in the * object. * * Note: The second argument (int k) is not actually used by this class. It is here to satisfy the interface. * * @param theta person ability estimate. * @param iparam array of item parameters. * @param k response category * @param D scaling constant that is either 1 or 1.7 * @return an array of first partial derivatives (i.e. the gradientAt). */ public double[] gradient(double theta, double[] iparam, int k, double D) { double[] deriv = new double[numberOfParameters]; double a = iparam[0]; double b = iparam[1]; double c = iparam[2]; double x = iparam[3]; double w = D * (theta - b); double z = Math.exp(D * a * (theta - b)); double z2 = z * z; double d = 1 + z; double d2 = d * d; double xmc = x - c; deriv[0] = xmc * z * w / d - xmc * z2 * w / d2; deriv[1] = -(xmc * z * D * a / d - xmc * z2 * D * a / d2); deriv[2] = 1.0 - z / d; deriv[3] = z / d; if (k == 0) { deriv[0] = -deriv[0]; deriv[1] = -deriv[1]; deriv[2] = -deriv[2]; deriv[3] = -deriv[3]; } return deriv; } /** * Computes gradientAt using item parameters stored in teh object. * * @param theta person ability value * @param k response category * @return gradientAt */ public double[] gradient(double theta, int k) { double[] iparam = { discrimination, difficulty, guessing, slipping }; return gradient(theta, iparam, k, D); } public double derivTheta(double theta) { return Double.NaN; } public double itemInformationAt(double theta) { return Double.NaN; } 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]); if (guessingPrior != null) p[2] = guessingPrior.nearestNonZero(param[2]); if (slippingPrior != null) p[3] = slippingPrior.nearestNonZero(param[3]); return p; } public void setDiscriminationPrior(ItemParamPrior discriminationPrior) { this.discriminationPrior = discriminationPrior; } public void setDifficultyPrior(ItemParamPrior difficultyPrior) { this.difficultyPrior = difficultyPrior; } public void setGuessingPrior(ItemParamPrior guessingPrior) { this.guessingPrior = guessingPrior; } public void setSlippingPrior(ItemParamPrior slippingPrior) { this.slippingPrior = slippingPrior; } public void setStepPriorAt(ItemParamPrior prior, int index) { } public ItemParamPrior getDiscriminationPrior() { return discriminationPrior; } public ItemParamPrior getDifficultyPrior() { return difficultyPrior; } public ItemParamPrior getGuessingPrior() { return guessingPrior; } public ItemParamPrior getSlippingPrior() { return slippingPrior; } public double addPriorsToLogLikelihood(double loglike, double[] iparam) { double priorProb = 0.0; double ll = loglike; //discrimination prior if (discriminationPrior != null) { priorProb = discriminationPrior.logDensity(iparam[0]); ll += priorProb; } //difficulty prior if (difficultyPrior != null) { priorProb = difficultyPrior.logDensity(iparam[1]); ll += priorProb; } //guessing prior if (guessingPrior != null) { priorProb = guessingPrior.logDensity(iparam[2]); ll += priorProb; } //slipping prior if (slippingPrior != null) { priorProb = slippingPrior.logDensity(iparam[3]); ll += priorProb; } return ll; } public double[] addPriorsToLogLikelihoodGradient(double[] loglikegrad, double[] iparam) { double[] llg = loglikegrad; if (discriminationPrior != null) { llg[0] -= discriminationPrior.logDensityDeriv1(iparam[0]); } if (difficultyPrior != null) { llg[1] -= difficultyPrior.logDensityDeriv1(iparam[1]); } if (guessingPrior != null) { llg[2] -= guessingPrior.logDensityDeriv1(iparam[2]); } if (slippingPrior != null) { llg[3] -= slippingPrior.logDensityDeriv1(iparam[3]); } return llg; } //======================================================================================================================= // Methods related to scale linking //======================================================================================================================= /** * Mean/sigma linking coefficients are computed from the mean and standard deviation of item difficulty. * The summary statistics are computed in a storeless manner. This method allows for the incremental * update to item difficulty summary statistics by combining them with other summary statistics. * * @param mean item difficulty mean. * @param sd item difficulty standard deviation. */ public void incrementMeanSigma(Mean mean, StandardDeviation sd) {//TODO check for correctness mean.increment(difficulty); sd.increment(difficulty); } /** * Mean/mean linking coefficients are computed from the mean item difficulty and mean item discrimination. * The summary statistics are computed in a storeless manner. This method allows for the incremental * update to item difficulty summary statistics by combining them with other summary statistics. * * @param meanDiscrimination item discrimination mean. * @param meanDifficulty item difficulty mean. */ public void incrementMeanMean(Mean meanDiscrimination, Mean meanDifficulty) {//TODO check for correctness meanDiscrimination.increment(discrimination); meanDifficulty.increment(difficulty); } /** * Computes probability of a response under a linear transformation. This method is mainly used for the * characteristic curve linking methods (see {@link com.itemanalysis.psychometrics.irt.equating.StockingLordMethod}). * It applies the linear transformation such that the New form is transformed to the Old Form. * * @param theta examinee proficiency value * @param response target category * @param intercept linking coefficient for intercept * @param slope linking coefficient for slope * @return */ public double tStarProbability(double theta, int response, double intercept, double slope) {//TODO check for correctness if (response == 1) return tStarProbRight(theta, intercept, slope); return tStarProbWrong(theta, intercept, slope); } private double tStarProbRight(double theta, double intercept, double slope) {//TODO check for correctness double a = discrimination / slope; double b = difficulty * slope + intercept; double top = Math.exp(D * a * (theta - b)); double prob = slipping + (guessing - slipping) * top / (1 + top); return prob; } private double tStarProbWrong(double theta, double intercept, double slope) {//TODO check for correctness return 1.0 - tStarProbRight(theta, intercept, slope); } /** * Computes probability of a response under a linear transformation. This method is mainly used for the * characteristic curve linking methods (see {@link com.itemanalysis.psychometrics.irt.equating.StockingLordMethod}). * It applies the linear transformation such that the Old form is transformed to the New Form. * * @param theta examinee proficiency value * @param response target category * @param intercept linking coefficient for intercept * @param slope linking coefficient for slope * @return */ public double tSharpProbability(double theta, int response, double intercept, double slope) {//TODO check for correctness if (response == 1) return tSharpProbRight(theta, intercept, slope); return tSharpProbWrong(theta, intercept, slope); } private double tSharpProbRight(double theta, double intercept, double slope) {//TODO check for correctness double a = discrimination * slope; double b = (difficulty - intercept) / slope; double top = Math.exp(D * a * (theta - b)); double prob = slipping + (guessing - slipping) * top / (1 + top); return prob; } private double tSharpProbWrong(double theta, double intercept, double slope) {//TODO check for correctness return 1.0 - tSharpProbRight(theta, intercept, slope); } /** * Computes item expected value under a linear transformation. This method is mainly used for the characteristic * curve linking methods (see {@link com.itemanalysis.psychometrics.irt.equating.StockingLordMethod}). * It applies the linear transformation such that the New form is transformed to the Old Form. * * @param theta person ability value * @param intercept intercept linking coefficient. * @param slope slope linking coefficient. * @return expected value under a linear transformation. */ public double tStarExpectedValue(double theta, double intercept, double slope) {//TODO check for correctness return tStarProbRight(theta, intercept, slope); } /** * Computes probability of a response under a linear transformation. This method is mainly used for the * characteristic curve linking methods (see {@link com.itemanalysis.psychometrics.irt.equating.StockingLordMethod}). * It applies the linear transformation such that the Old form is transformed to the New Form. * * @param theta examinee proficiency value * @param intercept linking coefficient for intercept * @param slope linking coefficient for slope * @return expected value under a linear transformation. */ public double tSharpExpectedValue(double theta, double intercept, double slope) {//TODO check for correctness return tSharpProbRight(theta, intercept, slope); } /** * Linear transformation of item parameters. * * @param intercept intercept transformation coefficient. * @param slope slope transformation coefficient. */ public void scale(double intercept, double slope) {//TODO check for correctness difficulty = intercept + slope * difficulty; discrimination = discrimination / slope; difficultyStdError *= slope; discriminationStdError *= slope; } //=====================================================================================================================// // GETTER AND SETTER METHODS MAINLY FOR USE WHEN ESTIMATING PARAMETERS // //=====================================================================================================================// public double[] getItemParameterArray() { double[] ip = new double[numberOfParameters]; ip[0] = discrimination; ip[1] = difficulty; ip[2] = guessing; ip[3] = slipping; return ip; } public void setStandardErrors(double[] x) { discriminationStdError = x[0]; difficultyStdError = x[1]; guessingStdError = x[2]; slippingStdError = x[3]; } /** * Gets the type of item response model. * * @return type of item response model. */ public IrmType getType() { return IrmType.L4; } /** * Gets the number of item parameters. * * @return number of item parameters. */ public int getNumberOfParameters() { return numberOfParameters; } public int getNumberOfEstimatedParameters() { if (isFixed) return 0; return numberOfParameters; } /** * Gets the item difficulty parameter. * * @return item difficulty. */ public double getDifficulty() { return difficulty; } /** * Set difficulty parameter to an existing value. If you are using this method to fix an item parameter during * estimation, you must also set the proposal value in {@link #setProposalDifficulty(double)}. * */ public void setDifficulty(double difficulty) { this.difficulty = difficulty; } /** * A proposal difficulty value is obtained during each iteration of the estimation routine. This method gets * the proposal item difficulty values. This method is needed for estimating item difficulty. * * @return proposed item difficulty value. */ public double getProposalDifficulty() { return proposalDifficulty; } /** * A proposal difficulty value is obtained during each iteration of the estimation routine. This method * sets the proposal value. * * @param proposalDifficulty proposed item difficulty value. */ public void setProposalDifficulty(double proposalDifficulty) { if (!isFixed) this.proposalDifficulty = proposalDifficulty; } /** * Gets the item difficulty standard error. * * @return item difficulty standard error. */ public double getDifficultyStdError() { return difficultyStdError; } /** * Item difficulty standard error may be computed external to the class. This method sets the difficulty * standard error to a computed value. * * @param stdError item difficulty standard error. */ public void setDifficultyStdError(double stdError) { difficultyStdError = stdError; } /** * Gets item discrimination. * * @return item discrimination. */ public double getDiscrimination() { return discrimination; } /** * Set discrimination parameter to an existing value. If you are using this method to fix an item parameter * during estimation, you must also set the proposal value with {@link #setProposalDiscrimination(double)}. * */ public void setDiscrimination(double discrimination) { this.discrimination = discrimination; } public double getProposalDiscrimination() { return proposalDiscrimination; } /** * Set the proposed discrimination estimate. * * @param discrimination proposed item discrimination value. */ public void setProposalDiscrimination(double discrimination) { if (!isFixed) this.proposalDiscrimination = discrimination; } /** * Gets the standard error for the item discrimination estimate. * * @return item discrimination standard error. */ public double getDiscriminationStdError() { return discriminationStdError; } /** * The standard error may be computed external to the class. It can be set to a specific value with this method. * * @param stdError item discrimination standard error. */ public void setDiscriminationStdError(double stdError) { discriminationStdError = stdError; } /** * Gets the pseudo-guessing (i.e. lower asymptote) parameter. * * @return guessing parameter. */ public double getGuessing() { return guessing; } /** * Set lower asymptote parameter to an existing value. If you are using this method to fix an item parameter * during estimation, you must also set the proposal value in {@link #setProposalGuessing(double)}. * */ public void setGuessing(double guessing) { this.guessing = guessing; } /** * A proposal guessing parameter value is obtained during each iteration of the estimation routine. This method * sets the proposal value. * * @param guessing proposed guessing parameter estimate. */ public void setProposalGuessing(double guessing) { if (!isFixed) this.proposalGuessing = guessing; } /** * Gets the guessing parameter estimate standard error. * * @return guessing parameter estimate standard error. */ public double getGuessingStdError() { return guessingStdError; } /** * The guessing parameter standard error may be computed external to the class. Use this method to set the * standard error to a particular value. * * @param stdError standard error for the guessing parameter estimate. */ public void setGuessingStdError(double stdError) { guessingStdError = stdError; } /** * Gets the slipping (i.e. upper asymptote) parameter. * * @return guessing parameter. */ public double getSlipping() { return slipping; } /** * Set upper asymptote parameter to an existing value. If you are using this method to fix an item parameter * during estimation, you must also set the proposal value in {@link #setProposalGuessing(double)}. * */ public void setSlipping(double slipping) { this.slipping = slipping; } /** * A proposal slipping parameter value is obtained during each iteration of the estimation routine. This method * sets the proposal value. * * @param slipping proposed slipping parameter estimate. */ public void setProposalSlipping(double slipping) { if (!isFixed) this.proposalSlipping = slipping; } /** * Gets the guessing parameter estimate standard error. * * @return guessing parameter estimate standard error. */ public double getSlippingStdError() { return slippingStdError; } /** * The guessing parameter standard error may be computed external to the class. Use this method to set the * standard error to a particular value. * * @param stdError standard error for the slipping parameter estimate. */ public void setSlippingStdError(double stdError) { slippingStdError = stdError; } public double getScalingConstant() { return D; } /** * Proposal values for every item parameter are obtained at each iteration of the estimation routine. The * proposal values for each parameters are obtained for each in turn using the estimated values from the * previous iteration. For example, a proposal difficulty estimate for itemA is obtained in iteration k+1 * using estimates from iteration k. Then, a proposal difficulty estimate for itemB is obtained in iteration k+1 * using estimates from iteration k (even though a new estimate exists for itemA). After obtaining proposal * values for every item on the test, the proposal values can be accepted as the new parameter estimates. This * method must be called to accept the proposal values as the new estimates. * */ public double acceptAllProposalValues() { double max = 0; double delta = 0; delta = Math.abs(this.discrimination - proposalDiscrimination); if (proposalDiscrimination >= 1) delta /= proposalDiscrimination; max = Math.max(max, delta); this.discrimination = proposalDiscrimination; delta = Math.abs(this.difficulty - proposalDifficulty); if (proposalDifficulty >= 1) delta /= proposalDifficulty; max = Math.max(max, delta); this.difficulty = proposalDifficulty; delta = Math.abs(this.guessing - proposalGuessing); if (proposalGuessing >= 1) delta /= proposalGuessing; max = Math.max(max, delta); this.guessing = proposalGuessing; delta = Math.abs(this.slipping - proposalSlipping); if (proposalSlipping >= 1) delta /= proposalSlipping; max = Math.max(max, delta); this.slipping = proposalSlipping; return max; } public double[] getStepParameters() { 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[] getStepStdError() { double[] sp = new double[ncat]; for (int k = 0; k < ncat; k++) { sp[k] = Double.NaN; } return sp; } public double[] getThresholdParameters() { double[] t = { 0 }; return t; } public double[] getThresholdStdError() { double[] sp = new double[ncat]; for (int k = 0; k < ncat; k++) { sp[k] = Double.NaN; } return sp; } public void setThresholdStdError(double[] stdError) { } public void setStepParameters(double[] step) { } public void setProposalStepParameters(double[] step) { } public void setThresholdParameters(double[] thresholdParameters) { } public void setProposalThresholds(double[] thresholds) { } //=====================================================================================================================// // END GETTER AND SETTER METHODS // //=====================================================================================================================// /** * A string representaiton of the item parameters. Mainly used for printing and debugging. * * @return a string of item parameters. */ @Override public String toString() { StringBuilder sb = new StringBuilder(); Formatter f = new Formatter(sb); String name = ""; if (getName() != null) { name = getName().toString(); } f.format("%-18s", name); f.format("%2s", ""); f.format("%-3s", "L4"); f.format("%4s", ""); f.format("% 4.2f", getDiscrimination()); f.format("%1s", ""); f.format("(%4.2f)", getDiscriminationStdError()); f.format("%4s", ""); f.format("% 4.2f", getDifficulty()); f.format("%1s", ""); f.format("(%4.2f)", getDifficultyStdError()); f.format("%4s", ""); f.format("% 4.2f", getGuessing()); f.format("%1s", ""); f.format("(%4.2f)", getGuessingStdError()); f.format("%4s", ""); f.format("% 4.2f", getSlipping()); f.format("%1s", ""); f.format("(%4.2f)", getSlippingStdError()); f.format("%4s", ""); return f.toString(); // // // StringBuilder sb = new StringBuilder(); // Formatter f = new Formatter(sb); // // f.format("%10s", getName().toString());f.format("%2s", ": "); // f.format("%1s", "["); // f.format("% .6f", getDiscrimination()); f.format("%2s", ", "); // f.format("% .6f", getDifficulty()); f.format("%2s", ", "); // f.format("% .6f", getGuessing()); f.format("%2s", ", "); // f.format("% .6f", getSlipping());f.format("%1s", "]"); // f.format("%n"); // f.format("%10s", "");f.format("%2s", ""); // f.format("%1s", "("); // f.format("% .6f", getDiscriminationStdError()); f.format("%2s", ", "); // f.format("% .6f", getDifficultyStdError()); f.format("%2s", ", "); // f.format("% .6f", getGuessingStdError()); f.format("%2s", ", "); // f.format("% .6f", getSlippingStdError());f.format("%1s", ")"); // return f.toString(); } }