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.measurement; import com.itemanalysis.psychometrics.data.*; import org.apache.commons.math3.stat.descriptive.rank.Max; import org.apache.commons.math3.stat.descriptive.rank.Min; import org.apache.commons.math3.util.Pair; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * The default implementation of the ItemScoring interface. It maintains information * about the item and scores examinees responses. It also includes special codes * for omitted and not reached responses. These types of response codes are either * treated as missing data or scored as zero points. Missing data retun a value of * Double.NaN. * */ public class DefaultItemScoring implements ItemScoring { public TreeMap<Object, Category> categoryMap = null; /** * Largest obtainable score on the item */ private Max maximumPossibleScore = null; /** * Smallest possible score on the item */ private Min minimumPossibleScore = null; private TreeSet<Double> scoreLevels = null; private SpecialDataCodes specialDataCodes = null; private boolean isContinuous = false; public DefaultItemScoring() { this(false); } public DefaultItemScoring(boolean isContinuous) { this.isContinuous = isContinuous; categoryMap = new TreeMap<Object, Category>(new ItemResponseComparator()); maximumPossibleScore = new Max(); minimumPossibleScore = new Min(); specialDataCodes = new SpecialDataCodes(); scoreLevels = new TreeSet<Double>(); } public void addCategory(Category cat) { categoryMap.put(cat.responseValue(), cat); maximumPossibleScore.increment(cat.scoreValue()); minimumPossibleScore.increment(cat.scoreValue()); scoreLevels.add(cat.scoreValue()); } public void removeCategory(Category cat) { categoryMap.remove(cat); scoreLevels.remove(cat.scoreValue()); } public void addCategory(Object categoryID, double scoreValue) { System.out.println(categoryID instanceof Double); categoryMap.put(categoryID, new Category(categoryID, scoreValue)); maximumPossibleScore.increment(scoreValue); minimumPossibleScore.increment(scoreValue); scoreLevels.add(scoreValue); } public void clearCategory() { this.categoryMap.clear(); this.scoreLevels.clear(); this.categoryMap.clear(); maximumPossibleScore = new Max(); minimumPossibleScore = new Min(); } /** * Gets the number of response options. * * @return number of response options. */ public int numberOfCategories() { return categoryMap.size(); } public void isContinuous(boolean isContinuous) { this.isContinuous = isContinuous; } public boolean isContinuous() { return isContinuous; } /** * Gets the number of score levels. For a polytomous item, this number will be the same as the value returned by * {@link #numberOfCategories()} unless the score categories are collapsed. With collapsed score categories, * the number of score levels will be less than the number of response options. * * @return number of score levels. */ public int numberOfScoreLevels() { return scoreLevels.size(); } public double maximumPossibleScore() { return maximumPossibleScore.getResult(); } public double minimumPossibleScore() { return minimumPossibleScore.getResult(); } /** * Return category iterator for binary and polytomous items. * No categories exist for continuous items. * * @return */ public Iterator<Object> categoryIterator() { return categoryMap.keySet().iterator(); } public String getCategoryScoreString(Object response) { Category c = categoryMap.get(response); if (c == null) return ""; String s = c.responseValue().toString() + "(" + c.scoreValue() + ")"; return s; } public void setSpecialDataCodes(SpecialDataCodes specialDataCodes) { this.specialDataCodes = specialDataCodes; } /** * Missing responses, omitted responses, and not reached responses are scored according to the value in * the SepcialDataCodes object. * * @param response an item response that is either a Double or String * @return item score */ public double computeItemScore(Object response) { if (response == null) { return specialDataCodes.computeMissingScore(SpecialDataCodes.PERMANENT_MISSING_DATA_CODE); } if (specialDataCodes.isMissing(response)) { return specialDataCodes.computeMissingScore(response); } if (isContinuous) { return Double.parseDouble(response.toString()); } double score = Double.NaN; Category temp = categoryMap.get(response); if (temp == null) { //undefined categories scored same as missing data return specialDataCodes.computeMissingScore(SpecialDataCodes.PERMANENT_MISSING_DATA_CODE);//return missing score } else { score = temp.scoreValue(); } return score; } /** * Creates an array of category keys and their corresponding score for the response. * They are stored in an array of Pairs so that the category ID and score are kept together. * * @param response an item response * @return */ public Pair[] computeScoreVector(Object response) { Pair[] pair = new Pair[this.numberOfCategories()]; Pair tempPair = null; double score = 0; int index = 0; for (Object obj : categoryMap.keySet()) { score = computeCategoryScore(obj, response); tempPair = new Pair(obj, score); pair[index] = tempPair; index++; } return pair; } /** * Returns 1 if response == categoryId and 0 otherwise. * * @param categoryId * @param response * @return */ public double computeCategoryScore(Object categoryId, Object response) { if (categoryId.equals(response)) return 1.0; return 0.0; // Category temp = categories.get(categoryId); // return temp.categoryScore(response); } /** * This method return the answer key for an item. the answer key is * the category code with the highest score values. For a multiple-choice * question it will return the response options with a score of, say, 1 * where all other scores are 0. For a polytomous item it will return a * plus sign to indicate increasing order and a minus sign to indicate * reverse order. jMetrik uses this method to populate a table with * the answer key. * * @return answer key */ public String getAnswerKey() { // Category cat; double scoreValue = 0; double maxScore = Double.NEGATIVE_INFINITY; String answerKey = ""; if (binaryScoring()) { // for(Object o : categories.keySet()){ // cat = categories.get(o); // scoreValue = cat.scoreValue(); for (Object o : categoryMap.keySet()) { scoreValue = categoryMap.get(o).scoreValue(); if (scoreValue > maxScore) { maxScore = scoreValue; answerKey = o.toString(); } } } else { //determine if polytomous item is in ascending order or reverse order Set<Object> keySet = categoryMap.keySet(); Object[] obj = keySet.toArray(); Arrays.sort(obj); Category cat1 = categoryMap.get(obj[0]); Category cat2 = categoryMap.get(obj[obj.length - 1]); if (cat1.scoreValue() <= cat2.scoreValue()) { answerKey = "+";//ascending order double min = minimumPossibleScore(); if (min != 1) answerKey += (int) min; } else { answerKey = "-";//reverse order double max = maximumPossibleScore(); if (max != numberOfCategories()) answerKey += (int) max; } } return answerKey; } /** * Parses string from syntax file or database. * The original responses should be a comma delimited list * enclosed in paretheses, REGEX=\\((.+?(?=,|\\)))\\). The score values should also be * a comma delimited list enclosed in paretheses. The score rho * list can only contain numbers, REGEX=\\(([[-+]?[0-9]*\\.?[0-9]+(?=,|\\))]+?)\\). * Leading and trailing white spaces within each list are NOT permitted. * The original responses should be listed first, followed by * the score values. Both lists must be enclosed in parentheses. * This format allows an item to have one or more correct responses, partial * credit assigned to any or all responses, and polytomous item * categories to be collapsed. * * For example, (A,B,C,D) (0,1,0,0) * indicates the original responses A, B, C, and D are scored * 1, 0, 0, 0 (i.e. A is correct all others are incorrect). * As another example (A,B,C,D) (0,1,0,1) indicates * that the original values A and D are scored 1 but C and D are * scored 0. For polytomous items, (1,2,3,4) (1,2,3,4) indicates * that the responses 1, 2, 3, and 4 are scored 1, 2, 3 and 4. * A polytomous item category may also be collapsed. For example, * (1,2,3,4) (1,2,3,3) indicates that responses or 3 and 4 are scored 3, * thereby collapsing the third and fourth categories. * * INCORRECT: (A, B, C, D)(0, 1, 0, 0) because leading and trailing * spaces within the list are not permitted. * * Original and scored lists will be forced to have equal length by * adding spaces or zeros to the end of a list until both lists * are of equal length. * * */ public ItemType addAllCategories(String optionScoreKey, VariableType type) { if (type.getItemType() == ItemType.CONTINUOUS_ITEM && optionScoreKey.trim().equals("")) { clearCategory(); isContinuous = true; return ItemType.CONTINUOUS_ITEM; } if (optionScoreKey.trim().equals("")) return ItemType.NOT_ITEM; /** * regular expression for (A,B,C,D) (0,1,0,0) * Careful it erroneously also matches trailing commas (A,B,C,D,) (0,1,0,0,) * * Group 1 = original rho list, \\((.+?(?=,|\\)))\\) * Group 2 = score rho list. Can only include numbers, \\(([[-+]?[0-9]*\\.?[0-9]+(?=,|\\))]+?)\\) * Both lists must be enclosed in parentheses. * */ String REGEX = "\\((.+?(?=,|\\)))\\)\\s*\\(([[-+]?[0-9]*\\.?[0-9]+(?=,|\\))]+?)\\)"; Pattern pattern = Pattern.compile(REGEX); String clean = optionScoreKey.trim(); Matcher matcher = pattern.matcher(clean); int matchCount = 0; String original = ""; String scoring = ""; while (matcher.find()) { original = matcher.group(1); scoring = matcher.group(2); matchCount++; } if (matchCount != 1) return ItemType.NOT_ITEM;//none or multiple matches found - should only be one match - format problem if (original.trim().equals("") || scoring.trim().equals("")) { clearCategory(); return ItemType.NOT_ITEM; } String[] orig = original.split(","); String[] scor = scoring.split(","); int origLength = orig.length; int scorLengh = scor.length; int maxLength = Math.max(origLength, scorLengh); String[] newOrig = newOrig = new String[maxLength]; String[] newScor = newScor = new String[maxLength]; int counter = 0; if (maxLength == origLength && maxLength == scorLengh) { for (int i = 0; i < maxLength; i++) { newOrig[i] = orig[i].trim(); newScor[i] = scor[i].trim(); } } else if (maxLength == scorLengh) { //scor is longer counter = 0; for (int i = 0; i < maxLength; i++) { if (counter < origLength) { newOrig[i] = orig[counter].trim(); newScor[i] = scor[i].trim(); } else { newOrig[i] = " "; newScor[i] = "0"; } counter++; } } else { //orig is longer counter = 0; for (int i = 0; i < maxLength; i++) { if (counter < scorLengh) { newOrig[i] = orig[i].trim(); newScor[i] = scor[counter].trim(); } else { newOrig[i] = orig[i].trim(); newScor[i] = "0"; } counter++; } } for (int i = 0; i < maxLength; i++) { Category cat = null; Double cScore = null; if (type.getDataType() == DataType.DOUBLE && !newOrig[i].equals("")) { cScore = Double.parseDouble(newScor[i]); cat = new Category(Double.parseDouble(newOrig[i]), cScore); } else { cScore = Double.parseDouble(newScor[i]); cat = new Category(newOrig[i], cScore); } this.addCategory(cat); // if(cScore==0 || cScore==1){ // binaryScoring+=0; // }else{ // binaryScoring++; // } } // if(binaryScoring==0)return ItemType.BINARY_ITEM; // return ItemType.POLYTOMOUS_ITEM; return getItemType(); } private int charaterCount(String text, char target) { char[] characters = text.toCharArray(); int count = 0; for (int i = 0; i < characters.length; i++) { if (characters[i] == target) count++; } return count; } /** * Determine whether the item scoring is binary or polytomous. * * @return true if binary scoring, false otherwise. */ public boolean binaryScoring() { // return binaryScoring==0; return getItemType() == ItemType.BINARY_ITEM; } /** * An item type is defined by its scoring. * A NOT_ITEM has no scoring assigned to the category. * A BINARY_ITEM has only two score levels with values 0 and 1. * A POLYTOMOUS_ITEM has more than two levels and each level is an integer. * A CONTINUOUS_ITEM has more than two levels and each level is a real number. Or, it is an item * set as continuous in the constructor or parsing of a score string. * * @return */ public ItemType getItemType() { // if(categories==null || categories.size()==0){ if (categoryMap == null || categoryMap.size() == 0) { return ItemType.NOT_ITEM; } else if (minimumPossibleScore() == 0 && maximumPossibleScore() == 1 && scoreLevels.size() == 2) { return ItemType.BINARY_ITEM; } else { if (isContinuous) return ItemType.CONTINUOUS_ITEM; for (Double d : scoreLevels) { if (d != Math.floor(d)) return ItemType.CONTINUOUS_ITEM; } return ItemType.POLYTOMOUS_ITEM; } } /** * this method creates a String that represents the item scoring. It * is referred to as the score string. * * @return */ public String printOptionScoreKey() { // if(categories.size()==0) return ""; if (categoryMap.size() == 0) return ""; // String tempCat = ""; String catOrig = "("; String catScor = "("; String finString = ""; Iterator<Object> iter = categoryMap.keySet().iterator(); Object obj = null; Category temp = null; String tempCat = ""; int nullCategory = 0; while (iter.hasNext()) { obj = iter.next(); temp = categoryMap.get(obj); tempCat = temp.responseValue().toString(); if (tempCat == null || tempCat.equals("")) nullCategory++; // if(temp == null || Double.isNaN(temp)) nullCategory++; // catOrig += obj.toString(); // catScor += temp.toString(); catOrig += tempCat; catScor += Double.valueOf(temp.scoreValue()).toString(); if (iter.hasNext()) { catOrig += ","; catScor += ","; } else { catOrig += ")"; catScor += ")"; } } finString = catOrig + " " + catScor; return finString; } /** * this method creates a double array of all existing options scores. The * options themselves are not returned. Only the scores are returned. * * @return array of options scores. */ public double[] scoreArray() { double[] s = new double[categoryMap.size()]; Iterator<Object> iter = categoryMap.keySet().iterator(); Category temp = null; int index = 0; while (iter.hasNext()) { temp = categoryMap.get(iter.next()); s[index] = temp.scoreValue(); index++; } Arrays.sort(s); return s; } /** * Sort Strings before Doubles. */ public class ItemResponseComparator implements Comparator<Object> { public int compare(Object obj1, Object obj2) { if (obj1 instanceof Double && obj2 instanceof Double) { return ((Double) obj1).compareTo((Double) obj2); } else if (obj1 instanceof String && obj2 instanceof Double) { return 1; } else if (obj1 instanceof Double && obj2 instanceof String) { return -1; } else { return obj1.toString().compareTo(obj2.toString()); } } } }