/* * Copyright (c) 2012 Patrick Meyer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.itemanalysis.jmetrik.graph.irt; import java.sql.*; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import javax.swing.SwingWorker; import com.itemanalysis.jmetrik.dao.DatabaseAccessObject; import com.itemanalysis.jmetrik.sql.DataTableName; import com.itemanalysis.jmetrik.sql.VariableTableName; import com.itemanalysis.jmetrik.stats.irt.linking.DbItemParameterSet; import com.itemanalysis.jmetrik.workspace.VariableChangeEvent; import com.itemanalysis.jmetrik.workspace.VariableChangeListener; import com.itemanalysis.psychometrics.data.VariableAttributes; import com.itemanalysis.psychometrics.data.VariableName; import com.itemanalysis.psychometrics.distribution.ContinuousDistributionApproximation; import com.itemanalysis.psychometrics.distribution.UniformDistributionApproximation; import com.itemanalysis.psychometrics.irt.estimation.IrtExaminee; import com.itemanalysis.psychometrics.irt.estimation.ItemResponseVector; import com.itemanalysis.psychometrics.irt.model.ItemResponseModel; import com.itemanalysis.psychometrics.statistics.TwoWayTable; import com.itemanalysis.psychometrics.tools.StopWatch; import com.itemanalysis.squiggle.base.SelectQuery; import com.itemanalysis.squiggle.base.Table; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import org.apache.log4j.Logger; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; public class IrtPlotAnalysis extends SwingWorker<IrtPlotPanel, Void> { private IrtPlotCommand command = null; private IrtPlotPanel irtPanel = null; private Throwable theException = null; private Connection conn = null; private int progressValue=0; private double maxProgress=100.0; private int lineNumber=0; private StopWatch sw = null; static Logger logger = Logger.getLogger("jmetrik-logger"); static Logger scriptLogger = Logger.getLogger("jmetrik-script-logger"); private DataTableName tableName = null; private ArrayList<String> variables = null; private boolean categoryProbability = true; private boolean plotIcc = true; private boolean plotItemInfo = false; private boolean plotTcc = true; private boolean plotTestInfo = false; private boolean plotTestStdError = false; private double[] theta = null; private double[] tcc = null; private double[] tinfo = null; private DatabaseAccessObject dao = null; private boolean savePlots = false; private String savePath = ""; private ItemResponseModel[] itemResponseModels = null; private DataTableName responseTableName = null; private boolean hasResponseData = false; private ItemResponseVector[] responseVector = null; private LinkedHashMap<String, ItemResponseModel> itemParameterSet = null; private ArrayList<VariableChangeListener> variableChangeListeners = null; int nBins = 10; double[] eap = null; DescriptiveStatistics eapStats = new DescriptiveStatistics(); TwoWayTable[] table = null; public IrtPlotAnalysis(Connection conn, DatabaseAccessObject dao, IrtPlotCommand command, IrtPlotPanel irtPanel){ this.command = command; this.irtPanel = irtPanel; this.conn = conn; this.dao = dao; variables = new ArrayList<String>(); variableChangeListeners = new ArrayList<VariableChangeListener>(); } private void initializeProgress()throws SQLException { this.firePropertyChange("progress-on", null, null); int nrow = dao.getRowCount(conn, tableName); maxProgress = (double)nrow; } private void updateProgress(){ progressValue=(int)((100*((double)lineNumber+1.0))/maxProgress); setProgress(Math.max(0,Math.min(100,progressValue))); lineNumber++; } private void getItemResponseModels()throws SQLException{ ArrayList<VariableName> selectedVariableNames = new ArrayList<VariableName>(); for(String s : variables){ selectedVariableNames.add(new VariableName(s)); } DbItemParameterSet dbItemParameterSet = new DbItemParameterSet(); itemParameterSet = dbItemParameterSet.getItemParameters( conn, tableName, selectedVariableNames, true ); itemResponseModels = new ItemResponseModel[itemParameterSet.size()]; // int j=0; // for(String s : itemParameterSet.keySet()){ // itemResponseModels[j] = itemParameterSet.get(s); // j++; // } //In order of the item parameter table int j=0; for(String s : variables){ itemResponseModels[j] = itemParameterSet.get(s); j++; } } //NOTE: The scaling constant 1 or 1.7 is only incorporated if it is contained in the item parameter table. //There is no default for the scaling constant that is set here. public void summarize()throws SQLException{ initializeProgress(); if(plotTcc) tcc = new double[theta.length]; if(plotTestInfo || plotTestStdError) tinfo = new double[theta.length]; int j=0; double maxPossibleTestScore = 0.0; double maxItemScore = 1; XYSeriesCollection collection = null; ItemResponseModel irm = null; for(String s : itemParameterSet.keySet()){ collection = new XYSeriesCollection(); irm = itemParameterSet.get(s); maxPossibleTestScore += irm.getMaxScoreWeight(); int ncat = irm.getNcat(); //plot icc if(plotIcc){ if(ncat>2){ if(categoryProbability){ //plot all category probabilities for polytomous item for(int i=0;i<ncat;i++){ collection.addSeries(getCategoryProbability(irm, i)); } }else{ //plot expected value for polytomous item collection.addSeries(getExpectedValue(irm)); maxItemScore = irm.getMaxScoreWeight(); } }else{ //plot probability of a correct response for binary item collection.addSeries(getCategoryProbability(irm, 1)); } } irtPanel.updateOrdinate(s, 0.0, maxItemScore); if(plotItemInfo){ collection.addSeries(getItemInformation(irm)); if(!plotIcc){ irtPanel.setOrdinateLabel(s, "Item Information"); irtPanel.setOrdinateAutoRange(s, true); } } if(hasResponseData){ addObservedPoints(j, collection); irtPanel.updateDatasetLinesAndPoints(s, collection, collection.getSeriesCount()>2); }else{ irtPanel.updateDataset(s, collection, collection.getSeriesCount()>1); } if(plotTcc){ incrementTcc(irm); } if(plotTestInfo || plotTestStdError){ incrementTestInfo(irm); } j++; } if(plotTcc || plotTestInfo || plotTestStdError) { collection = new XYSeriesCollection(); if(plotTcc){ addTcc(collection); irtPanel.updateOrdinate("jmetrik-tcc-tif-tse", 0.0, maxPossibleTestScore); irtPanel.setOrdinateLabel("jmetrik-tcc-tif-tse", "True Score"); }else{ irtPanel.setOrdinateAutoRange("jmetrik-tcc-tif-tse", true); if(plotTestStdError && !plotTestInfo){ irtPanel.setOrdinateLabel("jmetrik-tcc-tif-tse", "Standard Error"); } if(plotTestInfo && !plotTestStdError ){ irtPanel.setOrdinateLabel("jmetrik-tcc-tif-tse", "Test Information"); } } if(plotTestInfo) addTestInfo(collection); if(plotTestStdError) addTestStdError(collection); irtPanel.updateDataset("jmetrik-tcc-tif-tse", collection, collection.getSeriesCount()>1); } } private ArrayList<VariableAttributes> reorderAttributes(ArrayList<VariableAttributes> variableAttributes){ ArrayList<VariableAttributes> orderedAttributes = new ArrayList<VariableAttributes>(); outer: for(int j=0;j<itemResponseModels.length;j++){ inner: for(VariableAttributes v : variableAttributes){ if(itemResponseModels[j].getName().equals(v.getName())){ orderedAttributes.add(v); break inner; } } } return orderedAttributes; } private void summarizeResponseData()throws SQLException{ this.firePropertyChange("progress-ind-on", null, null); Statement stmt = null; ResultSet rs = null; //Summarize item response vectors try{ int nrow = dao.getRowCount(conn, responseTableName); responseVector = new ItemResponseVector[nrow]; eap = new double[nrow]; IrtExaminee examinee = new IrtExaminee(itemResponseModels); //Selecte variables must be in same order as in the array of item response models VariableTableName variableTableName = new VariableTableName(responseTableName.toString()); ArrayList<VariableAttributes> variableAttributes = reorderAttributes(dao.getSelectedVariables(conn, variableTableName, variables)); //Query the db. Variables include the select items and the grouping variable is one is available. Table sqlTable = new Table(responseTableName.getNameForDatabase()); SelectQuery select = new SelectQuery(); for(VariableAttributes v : variableAttributes){ select.addColumn(sqlTable, v.getName().nameForDatabase()); } stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); rs=stmt.executeQuery(select.toString()); int i=0; int c = 0; int ncol = itemResponseModels.length; byte[] rv = null; Object response = null; ItemResponseVector iVec = null; while(rs.next()){ c = 0; rv = new byte[ncol]; for(VariableAttributes v : variableAttributes){ response = rs.getObject(v.getName().nameForDatabase()); if((response==null || response.equals("") || response.equals("NA"))){ rv[c] = -1;//code for omitted responses }else{ rv[c] = (byte)v.getItemScoring().computeItemScore(response); } c++; } iVec = new ItemResponseVector(rv, 1.0); responseVector[i] = iVec; //Compute EAP estimate and increment descriptive statistics examinee.setResponseVector(responseVector[i]); eap[i] = examinee.eapEstimate(0, 1, -5, 5, 51);//TODO allow user to set the arguments eapStats.addValue(eap[i]); i++; }//end data summary condenseObservedResponses(); }catch(SQLException ex){ throw(ex); }finally{ if(rs!=null) rs.close(); if(stmt!=null) stmt.close(); } } private void condenseObservedResponses(){ double response = 0; double min = eapStats.getMin(); double max = eapStats.getMax(); double midPoint = 0; //initialize tables table = new TwoWayTable[itemResponseModels.length]; for(int i=0;i<table.length;i++){ table[i] = new TwoWayTable(); } //Create two-way frequency tables for(int i=0;i<responseVector.length;i++){ midPoint = findMidPoint(eap[i], nBins, min, max); for(int j=0;j<responseVector[i].getNumberOfItems();j++){ response = responseVector[i].getResponseAt(j); if(response!=-1){ table[j].addValue(midPoint, Double.valueOf(response).byteValue()); } } } } private void addObservedPoints(int itemIndex, XYSeriesCollection collection){ TwoWayTable itemTable = table[itemIndex]; Iterator<Comparable<?>> rowIter = itemTable.rowValuesIterator(); Iterator<Comparable<?>> colIter = itemTable.colValuesIterator(); double r; double prop; while(colIter.hasNext()){ Comparable<?> ci = colIter.next(); int c = ((Byte)ci).intValue(); XYSeries series = new XYSeries(c+"p"); if((itemResponseModels[itemIndex].getNcat()>2) || (itemResponseModels[itemIndex].getNcat()==2 && c==1) ){ rowIter = itemTable.rowValuesIterator(); while(rowIter.hasNext()){ Comparable<?> ri = rowIter.next(); r = (Double)ri; prop = (double)itemTable.getCount(ri, ci)/(double)itemTable.getRowCount(ri); series.add(r, prop); } collection.addSeries(series); } } } private double findMidPoint(double x, int nBins, double min, double max){ double h = (max-min)/nBins; double upperBound; for(int i=0;i<nBins;i++){ upperBound = min+(i+1)*h; if(x <= upperBound) return upperBound-h/2; } return max-h/2; } private void addTcc(XYSeriesCollection collection){ XYSeries tccSeries = new XYSeries("TCC"); int index = 0; for(double t : theta){ tccSeries.add(t, tcc[index]); index++; } collection.addSeries(tccSeries); } private void addTestInfo(XYSeriesCollection collection){ XYSeries tifSeries = new XYSeries("TIF"); int index = 0; for(double t : theta){ tifSeries.add(t, tinfo[index]); index++; } collection.addSeries(tifSeries); } private void addTestStdError(XYSeriesCollection collection){ XYSeries tseSeries = new XYSeries("TSE"); int index = 0; for(double t : theta){ tseSeries.add(t, 1.0/Math.sqrt(tinfo[index])); index++; } collection.addSeries(tseSeries); } private XYSeries getCategoryProbability(ItemResponseModel irm, int category){ XYSeries series = new XYSeries(category); double prob = 0.0; for(double t : theta){ prob = irm.probability(t, category); series.add(t, prob); } return series; } private XYSeries getExpectedValue(ItemResponseModel irm){ XYSeries series = new XYSeries("Expected Value"); double value = 0.0; for(double t : theta){ value = irm.expectedValue(t); series.add(t, value); } return series; } private void incrementTcc(ItemResponseModel irm){ double value = 0.0; int index = 0; for(double t : theta){ value = irm.expectedValue(t); tcc[index] += value; index++; } } private XYSeries getItemInformation(ItemResponseModel irm){ XYSeries series = new XYSeries("Information"); double info = 0.0; for(double t : theta){ info = irm.itemInformationAt(t); series.add(t, info); } return series; } private void incrementTestInfo(ItemResponseModel irm){ double value = 0.0; int index = 0; for(double t : theta){ value = irm.itemInformationAt(t); tinfo[index] += value; index++; } } protected IrtPlotPanel doInBackground(){ sw = new StopWatch(); this.firePropertyChange("status", "", "Running Irt Plot..."); try{ //database tableName = new DataTableName(command.getPairedOptionList("data").getStringAt("table")); //parse command variables = command.getFreeOptionList("variables").getString(); categoryProbability = command.getSelectOneOption("type").isValueSelected("prob"); plotIcc = command.getSelectAllOption("item").isArgumentSelected("icc"); plotItemInfo = command.getSelectAllOption("item").isArgumentSelected("info"); plotTcc = command.getSelectAllOption("person").isArgumentSelected("tcc"); plotTestInfo = command.getSelectAllOption("person").isArgumentSelected("info"); plotTestStdError = command.getSelectAllOption("person").isArgumentSelected("se"); double min = command.getPairedOptionList("xaxis").getDoubleAt("min").doubleValue(); double max = command.getPairedOptionList("xaxis").getDoubleAt("max").doubleValue(); int points = command.getPairedOptionList("xaxis").getIntegerAt("points").intValue(); savePlots = command.getFreeOption("output").hasValue(); if(savePlots) savePath = command.getFreeOption("output").getString(); getItemResponseModels(); hasResponseData = command.getPairedOptionList("response").hasValue(); if(!categoryProbability) hasResponseData = false;//Do not add observed proportions when expected value selected if(hasResponseData){ String tn = command.getPairedOptionList("response").getStringAt("table"); responseTableName = new DataTableName(tn); nBins = command.getPairedOptionList("response").getIntegerAt("bins"); summarizeResponseData(); } UniformDistributionApproximation thetaDist = new UniformDistributionApproximation(min, max, points); theta = thetaDist.getPoints(); summarize(); if(savePlots){ this.firePropertyChange("progress-ind-on", null, null); firePropertyChange("status", "", "Saving plots"); irtPanel.savePlots(savePath); } firePropertyChange("status", "", "Done: " + sw.getElapsedTime()); firePropertyChange("progress-off", null, null); //make statusbar progress not visible }catch(Throwable t){ logger.fatal(t.getMessage(), t); theException=t; } return irtPanel; } //=============================================================================================================== //Handle variable changes here //=============================================================================================================== public synchronized void addVariableChangeListener(VariableChangeListener l){ variableChangeListeners.add(l); } public synchronized void removeVariableChangeListener(VariableChangeListener l){ variableChangeListeners.remove(l); } public synchronized void removeAllVariableChangeListeners(){ variableChangeListeners.clear(); } public void fireVariableChanged(VariableChangeEvent event){ for(VariableChangeListener l : variableChangeListeners){ l.variableChanged(event); } } //=============================================================================================================== @Override protected void done(){ try{ if(theException!=null){ logger.fatal(theException.getMessage(), theException); firePropertyChange("error", "", "Error - Check log for details."); } scriptLogger.info(command.paste()); }catch(Exception ex){ logger.fatal(theException.getMessage(), theException); firePropertyChange("error", "", "Error - Check log for details."); } } }