com.itemanalysis.psychometrics.irt.estimation.JointMaximumLikelihoodEstimation.java Source code

Java tutorial

Introduction

Here is the source code for com.itemanalysis.psychometrics.irt.estimation.JointMaximumLikelihoodEstimation.java

Source

/*
 * Copyright 2013 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.estimation;

import com.itemanalysis.psychometrics.data.VariableName;
import com.itemanalysis.psychometrics.irt.model.ItemResponseModel;
import com.itemanalysis.psychometrics.irt.model.RaschRatingScaleGroup;
import com.itemanalysis.psychometrics.scaling.DefaultLinearTransformation;
import com.itemanalysis.psychometrics.texttable.TextTable;
import com.itemanalysis.psychometrics.texttable.TextTableColumnFormat;
import com.itemanalysis.psychometrics.texttable.TextTablePosition;
import org.apache.commons.math3.exception.DimensionMismatchException;
import org.apache.commons.math3.exception.NoDataException;
import org.apache.commons.math3.stat.descriptive.moment.Mean;

import java.util.ArrayList;
import java.util.Formatter;
import java.util.LinkedHashMap;

/**
 * Joint maximum likelihood estimation (JMLE) of the Rasch, partial credit, and rating scale models. Methods conduct item
 * and person parameter estimation, computation of standard errors, and linear transformations of parameters. Fixed
 * common item calibration is also possible. JMLE is an iterative procedure. At each iteration, item and person
 * parameters estimates are updated using a proportional curve fitting algorithm instead of the Newton-Raphson method.
 * For details on the estimation algorithm see:
 * <p>
 * Meyer, J. P., & Hailey, E. (2012). A study of Rasch partial credit, and rating scale model<br />
 *  &nbsp;&nbsp;&nbsp;&nbsp;parameter recovery in WINSTEPS and jMetrik. <em>Journal of Applied Measurement</em>,<br />
 *  &nbsp;&nbsp;&nbsp;&nbsp;<em>13</em>(3), 248-258.
 * </p>
 * In additional to parameter estimates, this class also provides a way to obtain item and person fit statistics and
 * scale quality statistics.
 *
 * @Author J. Patrick Meyer
 */
public class JointMaximumLikelihoodEstimation {

    private byte data[][] = null;
    private double[] theta = null;
    private double[] thetaStdError = null;
    private double[] sumScore = null;
    private ArrayList<IterationRecord> iterationHistory = null;
    private int[] extremePerson = null;
    private int[] extremeItem = null;
    private int[] droppedStatus = null;
    private ItemResponseModel[] irm = null;
    private RaschFitStatistics[] itemFit = null;
    private ItemResponseSummary[] itemSummary = null;
    private LinkedHashMap<String, RaschRatingScaleGroup> rsg = null;
    private int nPeople = 0;
    private int nItems = 0;
    private int maxCategory = 2;
    private double[] minPossibleTestScore = null;//sum of min score points for nonextreme items completed by the examinee
    private double[] maxPossibleTestScore = null;//sum of max score points for nonextreme items completed by the examinee
    private double adjustment = 0.3;
    private int extremeCount = 0;
    private int droppedCount = 0;

    /**
     * The object is created with a set of data and array of item response models (See {@link ItemResponseModel}.
     * The data must be arranged such that each row respresents an examinee and each column represents an item.
     * All item responses must be zero or larger (i.e. nonnegative). Use an item response value of -1 to indicate
     * a missing item response. The length of the item response model array must equal the number of columns in
     * the data array.
     *
     * @param data a two-way array of item responses.
     * @param irm an array of item response model.
     * @throws DimensionMismatchException
     */
    public JointMaximumLikelihoodEstimation(byte[][] data, ItemResponseModel[] irm)
            throws DimensionMismatchException {
        if (data[0].length != irm.length)
            throw new DimensionMismatchException(data[0].length, irm.length);
        this.irm = irm;
        this.nPeople = data.length;
        this.data = data;
        iterationHistory = new ArrayList<IterationRecord>();
        initializeCounts();
    }

    private void initializeCounts() {
        this.nItems = irm.length;
        this.extremeItem = new int[nItems];
        this.droppedStatus = new int[nItems];
        this.itemSummary = new ItemResponseSummary[nItems];
        this.extremePerson = new int[nPeople];
        this.sumScore = new double[nPeople];
        this.minPossibleTestScore = new double[nPeople];
        this.maxPossibleTestScore = new double[nPeople];
        rsg = new LinkedHashMap<String, RaschRatingScaleGroup>();

        //initialize item summaries
        ItemResponseSummary temp = null;
        VariableName vName = null;
        String groupId = "";
        for (int j = 0; j < nItems; j++) {
            vName = irm[j].getName();
            groupId = irm[j].getGroupId();
            temp = new ItemResponseSummary(vName, groupId, adjustment, irm[j].getScoreWeights());
            temp.setPositionInArray(j);
            maxCategory = Math.max(maxCategory, irm[j].getNcat());
            itemSummary[j] = temp;
            extremeItem[j] = 0;
        }

        //?? do I need this??
        for (int i = 0; i < nPeople; i++) {
            extremePerson[i] = 0;
        }
    }

    /**
     * summarizeData() is recursive. This helper method clears the scores before each iteration of summarizeData().
     */
    private void clearScores() {
        for (int i = 0; i < nPeople; i++) {
            sumScore[i] = 0;
            minPossibleTestScore[i] = 0;
            maxPossibleTestScore[i] = 0;
        }
        for (int j = 0; j < nItems; j++) {
            itemSummary[j].clearCounts();
        }
        for (String s : rsg.keySet()) {
            rsg.get(s).clearCounts();
        }

        rsg.clear();
    }

    /**
     * Summarizes data into frequency counts. It keeps track of extreme persons (i.e. examinees that
     * earn the minimum or maximum possible test score) and extreme items (i.e. items answered correctly by
     * everyone or no one). Extreme items and persons are flagged. Frequency counts represent the valid
     * counts (i.e. frequencies for non-extreme persons and items). This method is recursive. It continues
     * as long as new extreme items or persons are detected. Otherwise, it stops.
     *
     * @param adjustment and adjustmnet factor applied to the ItemResponseSummary. This adjustment is needed
     *                   for estimation of extreme persons and items.
     */
    public void summarizeData(double adjustment) throws NoDataException {
        int extremePersonCount = 0;
        int extremeItemCount = 0;
        int droppedItemCount = 0;
        clearScores();

        //summarize each item individually
        for (int i = 0; i < nPeople; i++) {
            for (int j = 0; j < nItems; j++) {
                if (data[i][j] != -1) {
                    if (droppedStatus[j] == 0 && extremeItem[j] == 0) {
                        //person scores should use only the nonextreme items
                        sumScore[i] += data[i][j];
                        minPossibleTestScore[i] += irm[j].getMinScoreWeight();
                        maxPossibleTestScore[i] += irm[j].getMaxScoreWeight();
                    }
                    if (extremePerson[i] == 0) {
                        //item scores should use only the nonextreme examinees
                        itemSummary[j].increment(data[i][j]);
                    }

                }

            }

            //check for extreme person
            if (sumScore[i] == minPossibleTestScore[i]) {
                extremePerson[i] = -1;
                extremePersonCount++;
            } else if (sumScore[i] == maxPossibleTestScore[i]) {
                extremePerson[i] = 1;
                extremePersonCount++;
            } else {
                extremePerson[i] += 0;
            }
        }

        //Combine items in the same rating scale group group.
        //Must be done after frequencies are tabulated for each item.
        String groupId = "";
        RaschRatingScaleGroup raschRatingScaleGroup = null;
        for (int j = 0; j < nItems; j++) {
            if (irm[j].getNcat() > 2) {
                groupId = irm[j].getGroupId();
                raschRatingScaleGroup = rsg.get(groupId);
                if (raschRatingScaleGroup == null) {
                    raschRatingScaleGroup = new RaschRatingScaleGroup(groupId, irm[j].getNcat());
                    rsg.put(groupId, raschRatingScaleGroup);
                }
                raschRatingScaleGroup.addItem(irm[j], itemSummary[j], j);
            }
        }

        //check for extreme items
        for (int j = 0; j < nItems; j++) {
            if (itemSummary[j].isExtremeMaximum()) {
                extremeItem[j] = 1;
                extremeItemCount++;
            } else if (itemSummary[j].isExtremeMinimum()) {
                extremeItem[j] = -1;
                extremeItemCount++;
            } else {
                extremeItem[j] += 0;
            }

            //check for extreme category
            if (irm[j].getNcat() > 2) {
                RaschRatingScaleGroup tempGroup = rsg.get(irm[j].getGroupId());
                tempGroup.checkForDroppping();
                droppedStatus[j] = tempGroup.dropStatus();
                if (droppedStatus[j] != 0)
                    droppedItemCount++;
            }

        } //end loop over items

        double priorCount = extremeCount;
        extremeCount = extremeItemCount + extremePersonCount;

        int priorDropCount = droppedCount;
        droppedCount = droppedItemCount;

        if (droppedItemCount != priorDropCount
                || (extremeCount != priorCount && extremeItemCount <= nItems && extremePersonCount <= nPeople)) {

            summarizeData(adjustment);

        }

        if (extremeItemCount == nItems || extremePersonCount == nPeople) {
            //no data remaining, throw an exception
            throw new NoDataException();
        }

    }

    /**
     * A shortcut method that only requires input for the maximum number of global iterations and the global
     * convergence criterion.
     *
     * @param globalMaxIter maximum number of iteration in JMLE
     * @param globalConv maximum change in logits convergen criterion
     * @return
     */
    public boolean estimateParameters(int globalMaxIter, double globalConv) {
        return estimateParameters(globalMaxIter, globalConv, 1, .01, true);
    }

    /**
     * A shortcut method that only requires input for the maximum number of global iterations and the global
     * convergence criterion.
     *
     * @param globalMaxIter maximum number of iteration in JMLE
     * @param globalConv maximum change in logits convergen criterion
     * @param centerItems establish identification by centering item about the item difficulty mean (the approach
     *                    typically used in Rasch measurement). If false establish identification by centering
     *                    persons around the mean ability. Centering only done for nonextreme persons and items.
     * @return
     */
    public boolean estimateParameters(int globalMaxIter, double globalConv, boolean centerItems) {
        return estimateParameters(globalMaxIter, globalConv, 1, .01, centerItems);
    }

    /**
     * This method is where the estimation is managed. It corresponds to the global iterations in the Joint
     * Maximum Likelihood Estimation paradigm. First items and thresholds are updated, then persons
     * are updated. These cycles continue until the number of iterations reaches the global maximum
     * or the convergence criterion is reached.
     *
     * @param globalMaxIter maximum number of iteration in JMLE
     * @param globalConv maximum change in logits convergen criterion
     * @param personMaxIter maximum number of iterations during the person update. Usually a maximum of one person
     *                      update is sufficient.
     * @param personConv convergence criterion during the person update only.
     * @param centerItems establish identification by centering item about the item difficulty mean (the approach
     *                    typically used in Rasch measurement). If false establish identification by centering
     *                    persons around the mean ability. Centering only done for nonextreme persons and items.
     * @return true if estimation is possible. False if data are not suited for estimation.
     */
    public boolean estimateParameters(int globalMaxIter, double globalConv, int personMaxIter, double personConv,
            boolean centerItems) {
        double delta = 1.0 + globalConv;
        double itemDelta = 0.0;
        double threshDelta = 0.0;
        double personDelta = 0.0;
        int iter = 0;

        //PROX starting values
        theta = new double[nPeople];
        thetaProx();

        while (delta > globalConv && iter < globalMaxIter) {

            //update items
            itemDelta = updateAllItems(delta, centerItems);

            //update thresholds
            if (maxCategory > 2) {
                //update thresholds
                for (String s : rsg.keySet()) {
                    threshDelta = updateThresholds(rsg.get(s));
                }
            }

            //accept proposal values
            for (int j = 0; j < nItems; j++) {
                irm[j].acceptAllProposalValues();
            }
            if (maxCategory > 2) {
                for (String s : rsg.keySet()) {
                    rsg.get(s).acceptAllProposalValues();
                }
            }

            //update persons
            personDelta = updateAllPersons(personMaxIter, personConv, adjustment, centerItems);

            //get max change in logits
            delta = Math.max(itemDelta, personDelta);
            iterationHistory.add(new IterationRecord(delta, getLogLikelihood()));
            iter++;
        }

        return true;

    }

    /**
     * This method manages the item updates during the JMLE iterations.
     *
     * @param delta current value of the maximum observed change in logits
     * @param centerItems establish identification by centering item about the item difficulty mean (the approach
     *                    typically used in Rasch measurement). If false establish identification by centering
     *                    persons around the mean ability. Centering only done for nonextreme persons and items.
     * @return maximum change in logits observed during the item updates
     */
    private double updateAllItems(double delta, boolean centerItems) {
        double maxDelta = 0.0;
        double difficulty = 0.0;
        double tempDifficulty = 0.0;
        Mean mean = new Mean();

        boolean hasFixed = false;

        //update each non extreme item
        for (int j = 0; j < nItems; j++) {
            if (droppedStatus[j] == 0) {
                tempDifficulty = updateDifficulty(irm[j], itemSummary[j], delta);
                if (extremeItem[j] == 0)
                    mean.increment(tempDifficulty);
            }
            if (irm[j].isFixed())
                hasFixed = true;
        }

        //Center non extreme items about the mean item difficulty.
        //Accept all proposal values.
        for (int j = 0; j < nItems; j++) {
            if (hasFixed) {
                //with fixed values, there is no need to constrain item difficulty to be zero.
                maxDelta = Math.max(maxDelta, Math.abs(irm[j].getProposalDifficulty() - irm[j].getDifficulty()));
            } else {
                //without fixed item parameter, constrain item difficulty to be zero.
                if (droppedStatus[j] == 0 && extremeItem[j] == 0) {
                    difficulty = irm[j].getDifficulty();
                    tempDifficulty = irm[j].getProposalDifficulty();
                    if (centerItems)
                        tempDifficulty -= mean.getResult();//center
                    irm[j].setProposalDifficulty(tempDifficulty);
                    maxDelta = Math.max(maxDelta, Math.abs(tempDifficulty - difficulty));
                }
            }

        }
        return maxDelta;
    }

    /**
     * Update of an individual item. The update only involves nonextreme examinees that responded to the item.
     *
     * @param irm an item response model.
     * @param isum an item response summary object with item frequencies.
     * @param delta current value of the observed maximum change in logits.
     * @return
     */
    private double updateDifficulty(ItemResponseModel irm, ItemResponseSummary isum, double delta) {
        if (irm.isFixed())
            return 0.0;
        double iTCC1 = 0.0;
        double iTCC2 = 0.0;
        double proposalDifficulty = 0.0;
        double difficulty = irm.getDifficulty();
        int pos = isum.getPositionInArray();

        for (int i = 0; i < nPeople; i++) {
            //update items using nonextreme people who completed hte item
            if (extremePerson[i] == 0 && data[i][pos] != -1) {
                iTCC1 += irm.expectedValue(theta[i]);
                irm.setDifficulty(difficulty + delta);
                iTCC2 += irm.expectedValue(theta[i]);
                irm.setDifficulty(difficulty);
            }
        }
        proposalDifficulty = curveFit(difficulty, isum.Sip(), delta, iTCC1, iTCC2, isum.minSip(), isum.maxSip());
        irm.setProposalDifficulty(proposalDifficulty);
        return proposalDifficulty;
    }

    /**
     * All thresholds updated here.
     *
     * @return largest change in threshold estimate.
     */
    private double updateAllThresholds() {
        double tDelta = 0.0;
        double maxDelta = 0.0;
        RaschRatingScaleGroup raschRatingScaleGroup = null;
        for (String s : rsg.keySet()) {
            raschRatingScaleGroup = rsg.get(s);
            if (raschRatingScaleGroup.dropStatus() == 0) {
                tDelta = updateThresholds(raschRatingScaleGroup);
                maxDelta = Math.max(maxDelta, tDelta);
                raschRatingScaleGroup.acceptAllProposalValues();
            }
        }
        return maxDelta;
    }

    /**
     * Thresholds for a single rating scale group are updated in this method. Updates only involve nonextreme
     * examinees that respond to the item.
     *
     * @param raschRatingScaleGroup group for which thresholds are updated.
     * @return maximum change in logits for this update.
     */
    private double updateThresholds(RaschRatingScaleGroup raschRatingScaleGroup) {
        double thresh = 0.0;
        int[] pos = raschRatingScaleGroup.getPositions();
        int nCat = raschRatingScaleGroup.getNumberOfCategories();
        double[] catKSum = new double[nCat];
        double[] thresholds = null;
        double[] proposalThresholds = new double[nCat - 1];
        Mean tMean = new Mean();
        double maxDelta = 0.0;
        thresholds = raschRatingScaleGroup.getThresholds();

        for (int i = 0; i < nPeople; i++) {
            if (extremePerson[i] == 0) {
                thresholds = raschRatingScaleGroup.getThresholds();
                for (int k = 0; k < nCat; k++) {
                    catKSum[k] += raschRatingScaleGroup.probabilitySumAt(theta[i], k);
                }
            }
        }

        int prevCat = 0;
        int nextCat = 0;
        for (int k = 0; k < nCat - 1; k++) {
            nextCat++;
            thresh = thresholds[k];
            proposalThresholds[k] = thresh
                    - Math.log(raschRatingScaleGroup.TpjAt(nextCat) / raschRatingScaleGroup.TpjAt(prevCat))
                    + Math.log(catKSum[nextCat] / catKSum[prevCat]);
            //do not change threshold by more than one logit - from WINSTEPS documentation
            proposalThresholds[k] = Math.max(Math.min(thresh + 1.0, proposalThresholds[k]), thresh - 1.0);
            tMean.increment(proposalThresholds[k]);
            prevCat = nextCat;
        }

        //recenter thresholds around the mean threshold
        double m = tMean.getResult();
        for (int k = 0; k < nCat - 1; k++) {
            proposalThresholds[k] = proposalThresholds[k] - m;
            maxDelta = Math.max(Math.abs(proposalThresholds[k] - thresholds[k]), maxDelta);
        }
        raschRatingScaleGroup.setProposalThresholds(proposalThresholds);

        return maxDelta;
    }

    /**
     * Update of all persons is handled here.
     *
     * @param maxIter maximum number of iteration in the person update.
     * @param converge convergence criterion for the person update. The criterion is the maximum change in logits.
     * @param adjustment extreme score adjustment.
     * @param centerItems establish identification by centering item about the item difficulty mean (the approach
     *                    typically used in Rasch measurement). If false establish identification by centering
     *                    persons around the mean ability. Centering only done for nonextreme persons and items.
     * @return maximum observed value change in logits from updating all examinees.
     */
    private double updateAllPersons(int maxIter, double converge, double adjustment, boolean centerItems) {
        double maxDelta = 0.0;
        double tempTheta = 0.0;
        Mean personMean = new Mean();
        for (int i = 0; i < nPeople; i++) {
            tempTheta = theta[i];
            theta[i] = updatePerson(i, maxIter, converge, adjustment);
            if (extremePerson[i] == 0) {
                personMean.increment(theta[i]);
                maxDelta = Math.max(Math.abs(theta[i] - tempTheta), maxDelta);
            }
        }

        if (!centerItems) {
            double m = personMean.getResult();
            for (int i = 0; i < nPeople; i++) {
                if (extremePerson[i] == 0)
                    theta[i] -= m;
            }
        }

        return maxDelta;
    }

    /**
     * An individual proficiency estimate is updated here.
     *
     * @param index array index of the person for which the update is computed.
     * @param maxIter maximum number of iteration in the person update.
     * @param converge convergence criterion for the person update. The criterion is the maximum change in logits.
     * @param adjustment extreme score adjustment.
     * @return newly updated theta value for computation of the maximum change in logits in updateAllPersons().
     */
    private double updatePerson(int index, int maxIter, double converge, double adjustment) {
        int iter = 0;
        double delta = 1.0 + converge;
        double previousTheta = 0.0;
        double TCC1 = 0.0; //this is the TCC at current theta rho
        double TCC2 = 0.0; //this is the TCC at current theta rho + d
        double shift = 0.0;

        //adjust extreme person scores
        double adjustedSumScore = sumScore[index];
        if (extremePerson[index] == -1) {
            adjustedSumScore += adjustment;
        } else if (extremePerson[index] == 1) {
            adjustedSumScore -= adjustment;
        }

        while (delta > converge && iter < maxIter) {
            previousTheta = theta[index];
            TCC1 = 0.0;
            TCC2 = 0.0;

            for (int j = 0; j < nItems; j++) {
                if (droppedStatus[j] == 0) {
                    if (extremeItem[j] == 0 && data[index][j] != -1) {
                        TCC1 += irm[j].expectedValue(theta[index]);
                        shift = theta[index] + delta;
                        TCC2 += irm[j].expectedValue(shift);
                    }
                }

            }

            theta[index] = curveFit(theta[index], adjustedSumScore, delta, TCC1, TCC2, minPossibleTestScore[index],
                    maxPossibleTestScore[index]);
            delta = Math.abs(previousTheta - theta[index]);
            iter++;
        }
        return theta[index];
    }

    /**
     * This implementation of JMLE involve proportional curve fitting. The curve fit is done in this method.
     * This method is called for updating item difficulty and person ability (i.e. theta).
     *
     * @param estimate current estimate
     * @param score person or item score.
     * @param delta Current change in logits. It gets smaller with each global iteration.
     * @param expected expected score based on the current parameter estimates
     * @param expectedPlus expected score  based on the current parameter estimates plus delta
     * @param minObserved minimum possible item or person score.
     * @param maxObserved maximum possible item or person score.
     * @return updated parameter estimate.
     */
    private double curveFit(double estimate, double score, double delta, double expected, double expectedPlus,
            double minObserved, double maxObserved) {
        double slope = delta / (logisticOgive(expectedPlus, minObserved, maxObserved)
                - logisticOgive(expected, minObserved, maxObserved));
        double intercept = estimate - slope * logisticOgive(expected, minObserved, maxObserved);
        double tempEstimate = slope * logisticOgive(score, minObserved, maxObserved) + intercept;
        //do not change theta by more than one logit per iteration - from WINSTEPS documents
        double newValue = Math.max(Math.min(estimate + 1, tempEstimate), estimate - 1);
        return newValue;
    }

    /**
     * Local logistic ogive from WINSTEPS documentation.
     *
     * @param x observed score
     * @param xMin minimum possible score
     * @param xMax maximum possible score
     * @return
     */
    private double logisticOgive(double x, double xMin, double xMax) {
        return Math.log((x - xMin) / (xMax - x));
    }

    /**
     * PROX start values for examinees.
     */
    private void thetaProx() {
        double aRS = 0.0;
        double thetaProx = 0.0;
        for (int i = 0; i < nPeople; i++) {
            aRS = sumScore[i];
            if (extremePerson[i] == -1) {
                aRS += adjustment;
            } else if (extremePerson[i] == 1) {
                aRS -= adjustment;
            }
            thetaProx = Math.log(aRS / (maxPossibleTestScore[i] - aRS));
            theta[i] = thetaProx;
        }

    }

    /**
     * Computes PROX difficulty estimates for item difficulty. These are used as starting values in JMLE.
     */
    public void itemProx() {
        for (int j = 0; j < nItems; j++) {
            if (droppedStatus[j] == 0 && !irm[j].isFixed()) {
                double maxItemScore = itemSummary[j].maxSip();
                double adjustedScore = itemSummary[j].Sip();
                double p = adjustedScore / maxItemScore;
                double q = 1.0 - p;
                double prox = Math.log(q / p);
                irm[j].setDifficulty(prox);
                irm[j].setProposalDifficulty(prox);

                int ncat = irm[j].getNcat();

                //threshold prox values
                if (ncat > 2) {
                    double previous = 0.0;
                    double current = 0.0;
                    double[] threshold = new double[ncat - 1];
                    RaschRatingScaleGroup group = rsg.get(irm[j].getGroupId());

                    Mean tMean = new Mean();
                    for (int k = 0; k < ncat; k++) {
                        current = group.SpjAt(k);
                        if (k > 0) {
                            threshold[k - 1] = Math.log(previous / current);
                            tMean.increment(threshold[k - 1]);
                        }
                        previous = current;
                    }

                    for (int k = 0; k < ncat - 1; k++) {
                        threshold[k] -= tMean.getResult();
                    }

                    irm[j].setThresholdParameters(threshold);
                    irm[j].setProposalThresholds(threshold);
                }

            }

        }

    }

    /**
     * Compute log-likelihood given the current values of item and person parameters.
     *
     * @return log-likelihood
     */
    public double getLogLikelihood() {
        double sum = 0.0;
        for (int i = 0; i < nPeople; i++) {
            for (int j = 0; j < nItems; j++) {
                if (droppedStatus[j] == 0) {
                    if (data[i][j] > -1) {
                        sum += Math.log(irm[j].probability(theta[i], data[i][j]));
                    }
                }

            }
        }
        return sum;
    }

    /**
     * Item response model objects are stored in an array. Gets the array of item response models.
     *
     * @return an array of item response models.
     */
    public ItemResponseModel[] getItems() {
        return irm;
    }

    /**
     * Person ability estimates are stored as an array of doubles. Get the array of person estimates.
     *
     * @return an array of person estimates.
     */
    public double[] getPersons() {
        return theta;
    }

    /**
     * Standard errors of person ability estimates are stored as an array of doubles. Gets the array of standard errors.
     * Will return null if {@link #computePersonStandardErrors} has not been called.
     *
     * @return
     */
    public double[] getPersonStdError() {
        return thetaStdError;
    }

    /**
     * Item difficulty standard error calculation. Standard errors for threshold parameters are also computed
     * for polytomous items.
     */
    public void computeItemStandardErrors() {
        double tempStdError = 0.0;
        for (int i = 0; i < nPeople; i++) {
            if (extremePerson[i] == 0) {
                for (int j = 0; j < nItems; j++) {
                    if (droppedStatus[j] == 0) {
                        if (data[i][j] != -1) {
                            tempStdError = irm[j].getDifficultyStdError();
                            tempStdError += irm[j].itemInformationAt(theta[i]);
                            irm[j].setDifficultyStdError(tempStdError);
                        }
                    }
                }
            }
        }
        for (int j = 0; j < nItems; j++) {
            if (droppedStatus[j] == 0) {
                tempStdError = irm[j].getDifficultyStdError();
                irm[j].setDifficultyStdError(1.0 / Math.sqrt(tempStdError));
            }
        }
        if (maxCategory > 2)
            computeAllThresholdStandardErrors();
    }

    /**
     * Item threshold standard error calculation.
     */
    private void computeAllThresholdStandardErrors() {
        RaschRatingScaleGroup gr = null;
        for (String s : rsg.keySet()) {
            gr = rsg.get(s);
            if (gr.dropStatus() == 0)
                gr.computeCategoryStandardError(theta, extremePerson);
        }
    }

    /**
     * Person ability parameter standard error calculation. Must be called before {@link #getPersonStdError}.
     */
    public void computePersonStandardErrors() {
        thetaStdError = new double[nPeople];
        double info = 0.0;
        for (int i = 0; i < nPeople; i++) {
            info = 0.0;
            for (int j = 0; j < nItems; j++) {
                if (data[i][j] != -1 && extremeItem[j] == 0 && droppedStatus[j] == 0) {
                    info += irm[j].itemInformationAt(theta[i]);
                }
            }
            thetaStdError[i] = 1.0 / Math.sqrt(info);
        }
    }

    //    public double[] getPersonStandardErrors(){
    //        if(thetaStdError==null) computePersonStandardErrors();
    //        return thetaStdError;
    //    }

    /**
     * An extreme iem is one in which all examinees with obtain the lowest possible item score or one in which
     * all examinee obtain the highest possible item score. Gets the count of extreme items.
     *
     * @return count of extreme items.
     */
    public int numberOfNonexremeItems() {
        int L = 0;
        for (int j = 0; j < extremeItem.length; j++) {
            if (extremeItem[j] == 0 && droppedStatus[j] == 0)
                L++;
        }
        return L;
    }

    /**
     * An extreme person is one that obtains the minimum possible test score or the maximum possible test score.
     * Gets the count of extreme persons.
     *
     * @return count of extreme persons.
     */
    public int numberOfNonextremePeople() {
        int N = 0;
        for (int i = 0; i < extremePerson.length; i++) {
            if (extremePerson[i] == 0)
                N++;
        }
        return N;
    }

    /**
     * Correct item difficulty and person ability estimates for bias. This is like using
     * the STBIAS=Y option in WINSTEPS.
     */
    public void biasCorrection() {
        double L = (double) numberOfNonexremeItems();
        double N = (double) numberOfNonextremePeople();
        double M = Math.min(2, maxCategory);
        double temp = 0.0;
        double itemAdjustment = ((M - 1) * (L - 1)) / ((M - 1) * (L - 1) + 1.0);
        double personAdjustment = ((M - 1) * (N - 1)) / ((M - 1) * (N - 1) + 1.0);

        //apply to items
        for (ItemResponseModel m : irm) {
            temp = m.getDifficulty();
            temp *= itemAdjustment;
            m.setDifficulty(temp);
        }

        for (int i = 0; i < nPeople; i++) {
            theta[i] = theta[i] * personAdjustment;
        }
    }

    /**
     * This method is for displaying frequency tables for each item. It is mainly used for
     * binary items and the frequencies involved in calculation of item difficulty.
     *
     * @return
     */
    public String printFrequencyTables() {
        String s = "";
        for (int j = 0; j < nItems; j++) {
            s += itemSummary[j].toString();
        }
        return s;
    }

    /**
     * This method is for displaying frequency tables for polytomous items. It will show combined
     * frequencies for items in the same rating scale group. These frequencies are mainly used
     * for estimation of threshold parameters.
     *
     * @return
     */
    public String printRatingScaleTables() {
        String out = "";
        for (String s : rsg.keySet()) {
            out += rsg.get(s).toString();
        }
        return out;
    }

    /**
     * Person ability estimates are stored in an array of doubles. Gets the entire arrya of person ability
     * estimates.
     *
     * @return person ability estimates.
     */
    public double[] getPersonEstimates() {
        return theta;
    }

    /**
     * Apply linear transformation to person and item parameter estimates. The transformation is
     * Xnew = X(scale) + intercept.
     *
     * @param intercept intercept transformation coefficient.
     * @param scale scale transformation coefficient.
     */
    public void linearTransformation(double intercept, double scale) {

        for (ItemResponseModel m : irm) {
            m.scale(intercept, scale);
        }

        for (int i = 0; i < nPeople; i++) {
            theta[i] = scale * theta[i] + intercept;
            thetaStdError[i] = thetaStdError[i] * scale;
        }
    }

    /**
     * After parameters have been estimated, fit statistics can be computed. Fit statistics include
     * INFIT and OUTFIT mean squared error statistic and their standardized versions. See
     * {@link RaschFitStatistics}. This method computes fit statistics for all items.
     */
    public void computeItemFitStatistics() {
        itemFit = new RaschFitStatistics[nItems];

        for (int j = 0; j < nItems; j++) {
            itemFit[j] = new RaschFitStatistics();
        }

        for (int i = 0; i < nPeople; i++) {
            if (extremePerson[i] == 0) {
                for (int j = 0; j < nItems; j++) {
                    if (data[i][j] > -1 && droppedStatus[j] == 0)
                        itemFit[j].increment(irm[j], theta[i], data[i][j]);
                }
            }
        }
    }

    /**
     * Computes person fit statistics for the examinee at the specified index. This method computes person
     * fit statistic for a single examinee.
     *
     * @param index array position of examinee for which the fit statistic is computed.
     * @return
     */
    public RaschFitStatistics getPersonFitStatisticsAt(int index) {
        RaschFitStatistics fit = new RaschFitStatistics();
        for (int j = 0; j < nItems; j++) {
            if (extremeItem[j] == 0) {
                if (data[index][j] > -1 && droppedStatus[j] == 0) {
                    fit.increment(irm[j], theta[index], data[index][j]);
                }
            }
        }
        return fit;
    }

    /**
     * After estimation, INFIT and OUTFIT mean square fit statistics for category thresholds can be computed.
     * This method computes category fit statistics for all items and categories.
     */
    public void computeItemCategoryFitStatistics() {
        if (maxCategory > 2) {
            RaschRatingScaleGroup g = null;
            for (int i = 0; i < nPeople; i++) {
                if (extremePerson[i] == 0) {
                    for (int j = 0; j < nItems; j++) {
                        if (data[i][j] > -1 && droppedStatus[j] == 0) {
                            g = rsg.get(irm[j].getGroupId());
                            if (g != null)
                                g.incrementFitStatistics(irm[j], theta[i], data[i][j]);
                        }
                    }
                }
            }
        }

    }

    /**
     * After estimation, overall indices of scale quality can be computed such as reliability and separation.
     * This method returns the scale quality statistics for the entire set of items.
     *
     * @return scale quality statistics for items.
     */
    public RaschScaleQualityStatistics getItemSideScaleQuality() {
        RaschScaleQualityStatistics iStats = new RaschScaleQualityStatistics();
        for (int j = 0; j < nItems; j++) {
            if (extremeItem[j] == 0 && droppedStatus[j] == 0) {
                iStats.increment(irm[j].getDifficulty(), irm[j].getDifficultyStdError());
            }
        }
        return iStats;
    }

    /**
     * After estimation, overall indices of scale quality can be computed such as reliability and separation.
     * This method returns the scale quality statistics for the entire group of examinees.
     *
     * @return
     */
    public RaschScaleQualityStatistics getPersonSideScaleQuality() {
        RaschScaleQualityStatistics pStats = new RaschScaleQualityStatistics();
        for (int i = 0; i < nPeople; i++) {
            if (extremePerson[i] == 0)
                pStats.increment(theta[i], thetaStdError[i]);
        }
        return pStats;
    }

    /**
     * Each item can have a different number of score categories. Gets the maximum score category.
     *
     * @return maximum score category among all items.
     */
    public int getMaxCategory() {
        return maxCategory;
    }

    /**
     * The total number of items on the test is stored. It includes all good items and all extreme items.
     *
     * @return total number of items.
     */
    public int getNumberOfItems() {
        return nItems;
    }

    /**
     * The total number of examinees is stored. It includes all extreme and nonextreme examinees.
     *
     * @return total number of examinees.
     */
    public int getNumberOfPeople() {
        return nPeople;
    }

    /**
     * An {@link ItemResponseModel} object is stored in an array for each item. Gets the item response model
     * at the given index.
     *
     * @param index array position of the item response model.
     * @return item response model at the given index.
     */
    public ItemResponseModel getItemResponseModelAt(int index) {
        return irm[index];
    }

    /**
     * Fit statistics for each item are stored in an array. Gets the fit statistics for the item in the array
     * at position given by the index.
     *
     * @param index array position of the item fit statistics.
     * @return fit statistics for an item.
     */
    public RaschFitStatistics getItemFitStatisticsAt(int index) {
        return itemFit[index];
    }

    /**
     * Polytomous response models be long to a single {@link RaschRatingScaleGroup}. Gets the rating scale
     * group for item at the indexed position in the array.
     *
     * @param index array position of the item.
     * @return rating scale group for the item.
     */
    public RaschRatingScaleGroup getRatingScaleGroupAt(int index) {
        String groupId = irm[index].getGroupId();
        RaschRatingScaleGroup group = rsg.get(groupId);
        return group;
    }

    /**
     * Extreme items are items in whih all examinees obtain the maximum possitble item score or the minimum possible
     * item score. An array stores flag as to whether items are extreme. Gets the indicator of the item's extreme
     * status.
     *
     * @param index position of item in the array.
     * @return extreme status for the item.
     */
    public int getExtremeItemAt(int index) {
        return extremeItem[index];
    }

    /**
     * Person ability estimates are stored in an array. Gets the estimate of ability for the person at the position
     * given by the index.
     *
     * @param index array position of the examinee.
     * @return current ability estimate for an examinee.
     */
    public double getPersonEstimateAt(int index) {
        return theta[index];
    }

    /**
     * Person estimate standard errors are stored in an array of doubles. Gets the standard error at the given position
     * of the array. The method {@link #computePersonStandardErrors} must be called before this method. Otherwise, it
     * will result in a null pointer exception.
     *
     * @param index position in array.
     * @return person estimate standard error.
     */
    public double getPersonEstimateStdErrorAt(int index) {
        return thetaStdError[index];
    }

    /**
     * The valid sum score is the sum score based on the nonextreme items. Valid sum scores are stored as an array
     * of doubles. Gets the valid sum score at the given position in the array.
     *
     * @param index position of valid sum score in the array.
     * @return valid sum score.
     */
    public double getValidSumScoreAt(int index) {
        return sumScore[index];
    }

    public double getSumScoreAt(int index) {
        double sum = 0.0;
        for (int j = 0; j < nItems; j++) {
            if (data[index][j] > -1)
                sum += data[index][j];
        }
        return sum;
    }

    /**
     * An extreme person is an examinee that obtains the minimum or maximum possible test score. Ability estimates
     * are not possible with extreme examinees without an adjustment. Extreme persons are flagged in a integer array
     * such that a code of 0 is a nonextreme examinee, -1 is an extreme minimum, and +1 is an extreme maximum.
     *
     * @param index position in array.
     * @return extreme person code.
     */
    public int getExtremePersonAt(int index) {
        return extremePerson[index];
    }

    /**
     * A residual is the difference between the observed and expected item response. Computes and returns the
     * residual for person i to item j.
     *
     * @param i index of examinee in data array.
     * @param j index of item in array of item response models.
     * @return residual for person i to item j.
     */
    public double getResidualAt(int i, int j) {
        if (data[i][j] == -1)
            return Double.NaN;
        double p = irm[j].expectedValue(theta[i]);
        double x = data[i][j];
        return x - p;
    }

    /**
     * A polytomous item with at least one category with 0 observations is dropped from the analysis. The dropped
     * status of each item is stored in an integer array. The dropped status is coded such that a value of 0 indicates
     * the item is not dropped, a value of -1 indicates a category with no observations, and a +1 indicates a category
     * with all of the observations (hence one or more other categories have 0 observations).
     *
     * @param index array index of item.
     * @return dropped status.
     */
    public int getDroppedStatusAt(int index) {
        return droppedStatus[index];
    }

    /**
     * This method is for displaying person parameter estimates, standard errors, and other information.
     * It will display statistic for every examinee.
     * @return
     */
    public String printPersonStats() {
        StringBuilder sb = new StringBuilder();
        Formatter f = new Formatter(sb);

        f.format("%12s", "Sum Score");
        f.format("%12s", "Theta");
        f.format("%12s", "Std. Error");
        f.format("%12s", "Extreme");
        f.format("%n");
        f.format("%50s", "--------------------------------------------------");
        f.format("%n");
        for (int i = 0; i < nPeople; i++) {
            f.format("%12.2f", sumScore[i]);
            f.format("%12.2f", theta[i]);
            f.format("%12.2f", thetaStdError[i]);
            if (extremePerson[i] == -1) {
                f.format("%12s", "MINIMUM");
            } else if (extremePerson[i] == 1) {
                f.format("%12s", "MAXIMUM");
            } else {
                f.format("%12s", "");
            }
            f.format("%n");
        }
        f.format("%50s", "--------------------------------------------------");
        f.format("%n");
        return f.toString();
    }

    public String printBasicItemStats() {
        return printItemStats("");
    }

    /**
     * This method is for displaying item parameter estimates, standard errors, and other information.
     *
     * @return
     */
    public String printBasicItemStats(String title) {
        StringBuilder sb = new StringBuilder();
        Formatter f = new Formatter(sb);
        int maxCat = 0;
        for (int j = 0; j < nItems; j++) {
            maxCat = Math.max(irm[j].getNcat(), maxCat);
        }

        f.format("%40s", title);
        f.format("%n");

        String line1 = "========================================================";
        int lineLength = 44;
        if (maxCat > 2) {
            for (int k = 0; k < maxCat - 1; k++) {
                line1 += "============";
                lineLength += 12;
            }
        }
        f.format("%" + lineLength + "s", line1);
        f.format("%n");

        f.format("%20s", "Name");
        f.format("%12s", "Difficulty");
        f.format("%12s", "Std. Error");
        if (maxCat > 2) {
            for (int k = 0; k < maxCat - 1; k++) {
                f.format("%12s", "step" + (k + 1));
            }
        }

        f.format("%12s", "Extreme");
        f.format("%n");

        String line = "--------------------------------------------------------";
        lineLength = 44;
        if (maxCat > 2) {
            for (int k = 0; k < maxCat - 1; k++) {
                line += "------------";
                lineLength += 12;
            }
        }

        f.format("%" + lineLength + "s", line);
        f.format("%n");
        for (int j = 0; j < nItems; j++) {
            f.format("%20s", irm[j].getName());

            if (droppedStatus[j] == 0) {
                f.format("%12.2f", irm[j].getDifficulty());
                f.format("%12.2f", irm[j].getDifficultyStdError());
                if (irm[j].getNcat() > 2) {
                    double[] t = irm[j].getThresholdParameters();
                    for (int k = 0; k < t.length; k++) {
                        f.format("%12.2f", t[k]);
                    }
                }

                if (itemSummary[j].isExtremeMaximum()) {
                    f.format("%12s", "MAXIMUM");
                } else if (itemSummary[j].isExtremeMinimum()) {
                    f.format("%12s", "MINIMUM");
                } else {
                    f.format("%12s", "");
                }
                f.format("%n");

                if (irm[j].getNcat() > 2) {
                    String id = irm[j].getGroupId();
                    double[] tse = rsg.get(id).getThresholdStandardError();
                    f.format("%44s", "");
                    for (int k = 0; k < tse.length; k++) {
                        f.format("%12.2f", tse[k]);
                    }
                    f.format("%n");
                }
            } else {
                f.format("%12s", "DROPPED");
                f.format("%n");
            }

        }
        f.format("%" + lineLength + "s", line1);
        f.format("%n");
        return f.toString();
    }

    /**
     * Joint maximum likelihood estimation is iterative. An iteration history is saved. The history includes
     * the largest change in logits and the log-likelihood at each iteration. This method provides a string
     * representation of the iteration history.
     *
     * @return a summary of the iteration history.
     */
    public String printIterationHistory() {
        StringBuilder sb = new StringBuilder();
        Formatter f = new Formatter(sb);
        int index = 0;

        f.format("%10s", "Iteration");
        f.format("%5s", "");
        f.format("%17s", "Delta");
        f.format("%5s", "");
        f.format("%20s", "Log-likelihood");
        f.format("%n");
        f.format("%62s", "--------------------------------------------------------------");
        f.format("%n");
        for (IterationRecord ir : iterationHistory) {
            f.format("%10s", ++index);
            f.format("%5s", "");
            f.format("%42s", ir.toString());
            f.format("%n");
        }
        f.format("%62s", "--------------------------------------------------------------");
        f.format("%n");
        return f.toString();
    }

    /**
     * The sum score is a sufficient statistic for the latent trait in the Rasch family of models. As such,
     * it is easy to create a table that shows the 1:1 correspondence between the sum score and person ability.
     * A score table is created showing this correspondence. It requires its own set of iterations, which is why
     * some parameters concern the iteration.
     *
     * @param maxIter maximum number of iterations to use when estimating ability.
     * @param converge convergence criterion when estimating ability.
     * @param adjustment extreme score adjustment that is applied to extreme persons.
     * @param transformation linear transformation to be applied to person ability estimates.
     * @param precision number of decimal places to use when reporting estimates int eh score table.
     * @return a score table that ranges from the minimum possible score to the maximum possible score.
     */
    public String printScoreTable(int maxIter, double converge, double adjustment,
            DefaultLinearTransformation transformation, int precision) {
        RaschScoreTable table = new RaschScoreTable(irm, extremeItem, droppedStatus);
        table.updateScoreTable(maxIter, converge, adjustment);
        table.computePersonStandardErrors();
        table.linearTransformation(transformation, precision);//must come after computation of standard errors
        return table.printScoreTable();
    }

    /**
     * Creates a string of item statistics. It includes (a) item name, (b) item difficulty, (c) difficulty
     * standard error, (d) four item fit statistics, and (e) a extreme item flag. This method for displaying
     * statistics is the one used in jMetrik for displaying results.
     *
     * @param title a title for the output table.
     * @return a table of item statistics.
     */
    public String printItemStats(String title) {
        TextTableColumnFormat[] cformats = new TextTableColumnFormat[8];
        cformats[0] = new TextTableColumnFormat();
        cformats[0].setStringFormat(10, TextTableColumnFormat.OutputAlignment.LEFT);
        cformats[1] = new TextTableColumnFormat();
        cformats[1].setDoubleFormat(10, 2, TextTableColumnFormat.OutputAlignment.RIGHT);
        cformats[2] = new TextTableColumnFormat();
        cformats[2].setDoubleFormat(10, 2, TextTableColumnFormat.OutputAlignment.RIGHT);
        cformats[3] = new TextTableColumnFormat();
        cformats[3].setDoubleFormat(8, 2, TextTableColumnFormat.OutputAlignment.RIGHT);
        cformats[4] = new TextTableColumnFormat();
        cformats[4].setDoubleFormat(8, 2, TextTableColumnFormat.OutputAlignment.RIGHT);
        cformats[5] = new TextTableColumnFormat();
        cformats[5].setDoubleFormat(8, 2, TextTableColumnFormat.OutputAlignment.RIGHT);
        cformats[6] = new TextTableColumnFormat();
        cformats[6].setDoubleFormat(8, 2, TextTableColumnFormat.OutputAlignment.RIGHT);
        cformats[7] = new TextTableColumnFormat();
        cformats[7].setStringFormat(8, TextTableColumnFormat.OutputAlignment.LEFT);

        TextTable textTable = new TextTable();
        textTable.addAllColumnFormats(cformats, nItems + 5);
        textTable.getRowAt(0).addHeader(0, 7, title, TextTablePosition.CENTER);
        textTable.getRowAt(1).addHorizontalRule(0, 7, "=");
        textTable.getRowAt(2).addHeader(0, 1, "Item", TextTablePosition.LEFT);
        textTable.getRowAt(2).addHeader(1, 1, "Difficulty", TextTablePosition.CENTER);
        textTable.getRowAt(2).addHeader(2, 1, "Std. Error", TextTablePosition.CENTER);
        textTable.getRowAt(2).addHeader(3, 1, "WMS", TextTablePosition.CENTER);
        textTable.getRowAt(2).addHeader(4, 1, "Std. WMS", TextTablePosition.CENTER);
        textTable.getRowAt(2).addHeader(5, 1, "UMS", TextTablePosition.CENTER);
        textTable.getRowAt(2).addHeader(6, 1, "Std. UMS", TextTablePosition.CENTER);
        textTable.getRowAt(3).addHorizontalRule(0, 7, "-");

        int index = 4;
        RaschRatingScaleGroup group = null;
        for (int j = 0; j < nItems; j++) {
            group = rsg.get(irm[j].getGroupId());
            textTable.getRowAt(index).addStringAt(0, irm[j].getName().toString());
            if (group != null && group.dropStatus() != 0) {
                textTable.getRowAt(index).addHeader(1, 6, "DROPPED", TextTablePosition.CENTER);
            } else {
                textTable.getRowAt(index).addDoubleAt(1, irm[j].getDifficulty());
                textTable.getRowAt(index).addDoubleAt(2, irm[j].getDifficultyStdError());
                if (itemFit != null) {
                    textTable.getRowAt(index).addDoubleAt(3, itemFit[j].getWeightedMeanSquare());
                    textTable.getRowAt(index).addDoubleAt(4, itemFit[j].getStandardizedWeightedMeanSquare());
                    textTable.getRowAt(index).addDoubleAt(5, itemFit[j].getUnweightedMeanSquare());
                    textTable.getRowAt(index).addDoubleAt(6, itemFit[j].getStandardizedUnweightedMeanSquare());
                } else {
                    textTable.getRowAt(index).addDoubleAt(3, Double.NaN);
                    textTable.getRowAt(index).addDoubleAt(4, Double.NaN);
                    textTable.getRowAt(index).addDoubleAt(5, Double.NaN);
                    textTable.getRowAt(index).addDoubleAt(6, Double.NaN);
                }

                if (extremeItem[j] == -1) {
                    textTable.getRowAt(index).addStringAt(7, "Minimum");
                } else if (extremeItem[j] == 1) {
                    textTable.getRowAt(index).addStringAt(7, "Maximum");
                }

            }
            index++;
        }
        textTable.getRowAt(nItems + 4).addHorizontalRule(0, 7, "=");

        String output = textTable.toString();
        if (droppedCount == 1) {
            output += "Item dropped due to unobserved categories. Collapse categories to retain item.";
        } else if (droppedCount > 1) {
            output += "Multiple items dropped due to unobserved categories. Collapse categories to retain items.";
        }

        return output;
    }

    /**
     * Polytomous items have a set of threshold statistics that must be reported. They are listed in a separate
     * output table as defined by this method.
     *
     * @return a table of category statistics.
     */
    public String printCategoryStats() {
        if (rsg.isEmpty())
            return "";
        TextTableColumnFormat[] cformats = new TextTableColumnFormat[6];
        cformats[0] = new TextTableColumnFormat();
        cformats[0].setStringFormat(10, TextTableColumnFormat.OutputAlignment.LEFT);
        cformats[1] = new TextTableColumnFormat();
        cformats[1].setIntFormat(10, TextTableColumnFormat.OutputAlignment.RIGHT);
        cformats[2] = new TextTableColumnFormat();
        cformats[2].setDoubleFormat(10, 2, TextTableColumnFormat.OutputAlignment.RIGHT);
        cformats[3] = new TextTableColumnFormat();
        cformats[3].setDoubleFormat(8, 2, TextTableColumnFormat.OutputAlignment.RIGHT);
        cformats[4] = new TextTableColumnFormat();
        cformats[4].setDoubleFormat(8, 2, TextTableColumnFormat.OutputAlignment.RIGHT);
        cformats[5] = new TextTableColumnFormat();
        cformats[5].setDoubleFormat(8, 2, TextTableColumnFormat.OutputAlignment.RIGHT);

        int numRows = 0;
        for (String s : rsg.keySet()) {
            numRows += rsg.get(s).getNumberOfCategories() + 1;
        }

        TextTable textTable = new TextTable();
        textTable.addAllColumnFormats(cformats, numRows + 5);
        textTable.getRowAt(0).addHeader(0, 8, "FINAL JMLE CATEGORY STATISTICS", TextTablePosition.CENTER);
        textTable.getRowAt(1).addHorizontalRule(0, 8, "=");
        textTable.getRowAt(2).addHeader(0, 1, "Group", TextTablePosition.LEFT);
        textTable.getRowAt(2).addHeader(1, 1, "Category", TextTablePosition.RIGHT);
        textTable.getRowAt(2).addHeader(2, 1, "Threshold", TextTablePosition.RIGHT);
        textTable.getRowAt(2).addHeader(3, 1, "Std. Error", TextTablePosition.RIGHT);
        textTable.getRowAt(2).addHeader(4, 1, "WMS", TextTablePosition.RIGHT);
        textTable.getRowAt(2).addHeader(5, 1, "UMS", TextTablePosition.RIGHT);
        textTable.getRowAt(3).addHorizontalRule(0, 6, "-");

        int index = 4;
        RaschRatingScaleGroup group = null;
        double[] thresholds = null;
        double[] thresholdsStdError = null;
        for (String s : rsg.keySet()) {
            group = rsg.get(s);
            thresholds = group.getThresholds();
            thresholdsStdError = group.getThresholdStandardError();
            for (int i = 0; i < group.getNumberOfCategories(); i++) {
                if (i == 0) {
                    textTable.getRowAt(index).addStringAt(0, s);
                    textTable.getRowAt(index).addIntAt(1, i);
                    if (group.dropStatus() != 0) {
                        textTable.getRowAt(index).addHeader(2, 4, "DROPPED", TextTablePosition.LEFT);
                    } else {
                        textTable.getRowAt(index).addHeader(2, 4, " ", TextTablePosition.LEFT);
                    }
                } else {
                    textTable.getRowAt(index).addStringAt(0, " ");
                    textTable.getRowAt(index).addIntAt(1, i);
                    if (group.dropStatus() != 0) {
                        textTable.getRowAt(index).addHeader(2, 1, " ", TextTablePosition.LEFT);
                        textTable.getRowAt(index).addHeader(3, 1, " ", TextTablePosition.LEFT);
                        textTable.getRowAt(index).addHeader(4, 1, " ", TextTablePosition.LEFT);
                        textTable.getRowAt(index).addHeader(5, 1, " ", TextTablePosition.LEFT);
                    } else {
                        textTable.getRowAt(index).addDoubleAt(2, thresholds[i - 1]);
                        textTable.getRowAt(index).addDoubleAt(3, thresholdsStdError[i - 1]);
                        textTable.getRowAt(index).addDoubleAt(4,
                                group.getCategoryFitAt(i - 1).getWeightedMeanSquare());
                        textTable.getRowAt(index).addDoubleAt(5,
                                group.getCategoryFitAt(i - 1).getUnweightedMeanSquare());
                    }
                }
                index++;
            }

            textTable.getRowAt(index).addHeader(0, 6, " ", TextTablePosition.LEFT);//add empty row
            index++;
        }
        textTable.getRowAt(numRows + 3).addHorizontalRule(0, 6, "=");
        return textTable.toString();
    }

}