/* GeoGebra - Dynamic Mathematics for Everyone http://www.geogebra.org This file is part of GeoGebra. 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. */ package org.geogebra.common.kernel.statistics; import java.util.ArrayList; import org.geogebra.common.kernel.Construction; import org.geogebra.common.kernel.StringTemplate; import org.geogebra.common.kernel.algos.AlgoElement; import org.geogebra.common.kernel.algos.TableAlgo; import org.geogebra.common.kernel.commands.Commands; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoList; import org.geogebra.common.kernel.geos.GeoNumeric; import org.geogebra.common.kernel.geos.GeoText; import org.geogebra.common.util.lang.Unicode; /** * ContingencyTable[] algorithm * * @author G. Sturr * */ public class AlgoContingencyTable extends AlgoElement implements TableAlgo { private GeoList list1, list2, rowList, colList, freqMatrix; // input private GeoText args; // input private GeoText table; // output // for compute private AlgoFrequency freq; private StringBuilder tableSb = new StringBuilder(); private boolean isRawData; private String[] rowValues; private String[] colValues; private int[][] freqValues; private double[][] expected; private double[][] chiCont; private int[] rowSum; private int[] colSum; private int totalSum; // display option flags private boolean showRowPercent, showColPercent, showTotalPercent, showChi, showExpected, showTest; private int rowCount; private int colCount; private int lastRow; /************************************************** * Constructs a contingency table from raw data * * @param cons * @param label * @param list1 * @param list2 * @param args * */ public AlgoContingencyTable(Construction cons, String label, GeoList list1, GeoList list2, GeoText args) { super(cons); isRawData = true; this.list1 = list1; this.list2 = list2; this.args = args; freq = new AlgoFrequency(cons, list1, list2, true); cons.removeFromConstructionList(freq); table = new GeoText(cons); setInputOutput(); // must set isLaTex before computing, #3846 table.isTextCommand = true; table.setLaTeX(true, false); compute(); table.setLabel(label); } /*************************************************** * Constructs a contingency table from a given frequency table * * @param cons * @param label * @param rowList * @param colList * @param freqMatrix * @param args */ public AlgoContingencyTable(Construction cons, String label, GeoList rowList, GeoList colList, GeoList freqMatrix, GeoText args) { super(cons); isRawData = false; this.rowList = rowList; this.colList = colList; this.freqMatrix = freqMatrix; this.args = args; table = new GeoText(cons); setInputOutput(); // must set isLaTex before computing, #3846 table.isTextCommand = true; table.setLaTeX(true, false); compute(); table.setLabel(label); } @Override public Commands getClassName() { return Commands.ContingencyTable; } @Override protected void setInputOutput() { ArrayList<GeoElement> outList = new ArrayList<GeoElement>(); if (list1 != null) { outList.add(list1); } if (list2 != null) { outList.add(list2); } if (rowList != null) { outList.add(rowList); } if (colList != null) { outList.add(colList); } if (freqMatrix != null) { outList.add(freqMatrix); } if (args != null) { outList.add(args); } input = new GeoElement[outList.size()]; input = outList.toArray(input); setOutputLength(1); setOutput(0, table); setDependencies(); // done by AlgoElement } public GeoText getResult() { return table; } private void parseArgs() { // set defaults showRowPercent = false; showColPercent = false; showTotalPercent = false; showChi = false; showExpected = false; showTest = false; lastRow = 0; if (args != null) { String optionsStr = args.getTextString(); if (optionsStr.indexOf("_") > -1) { showRowPercent = true; lastRow = 1; } if (optionsStr.indexOf("|") > -1) { showColPercent = true; lastRow = 2; } if (optionsStr.indexOf("+") > -1) { showTotalPercent = true; lastRow = 3; } if (optionsStr.indexOf("e") > -1) { showExpected = true; lastRow = 4; } if (optionsStr.indexOf("k") > -1) { showChi = true; lastRow = 5; } if (optionsStr.indexOf("=") > -1) { showTest = true; } } } /** * Loads raw data from GeoLists into arrays */ private boolean loadRawDataValues() { if (!freq.getResult().isDefined()) { return false; } rowValues = freq.getContingencyRowValues(); colValues = freq.getContingencyColumnValues(); GeoList fr = freq.getResult(); rowSum = new int[rowValues.length]; colSum = new int[colValues.length]; totalSum = 0; freqValues = new int[rowValues.length][colValues.length]; for (int rowIndex = 0; rowIndex < rowValues.length; rowIndex++) { GeoList rowGeo = (GeoList) fr.get(rowIndex); for (int colIndex = 0; colIndex < colValues.length; colIndex++) { freqValues[rowIndex][colIndex] = (int) ((GeoNumeric) rowGeo .get(colIndex)).getDouble(); rowSum[rowIndex] += freqValues[rowIndex][colIndex]; colSum[colIndex] += freqValues[rowIndex][colIndex]; totalSum += freqValues[rowIndex][colIndex]; } } return true; } /** * Loads prepared frequencies and values from GeoLists into arrays */ private boolean loadPreparedDataValues() { GeoElement geo; if (rowList == null || colList == null || freqMatrix == null || !rowList.isDefined() || !colList.isDefined() || !freqMatrix.isDefined() || !freqMatrix.isMatrix()) { table.setUndefined(); return false; } // TODO: reuse value arrays rowCount = rowList.size(); if (freqMatrix.size() != rowCount) { table.setUndefined(); return false; } colCount = colList.size(); rowValues = new String[rowCount]; colValues = new String[colCount]; rowSum = new int[rowCount]; colSum = new int[colCount]; for (int i = 0; i < rowCount; i++) { geo = rowList.get(i); if (!geo.isGeoText()) { return false; } rowValues[i] = ((GeoText) geo).getTextString(); } for (int i = 0; i < colCount; i++) { geo = colList.get(i); if (!geo.isGeoText()) { return false; } colValues[i] = ((GeoText) geo).getTextString(); } freqValues = new int[rowSum.length][colValues.length]; totalSum = 0; for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) { // row element GeoList rowGeo = (GeoList) freqMatrix.get(rowIndex); for (int colIndex = 0; colIndex < colCount; colIndex++) { // geo element geo = rowGeo.get(colIndex); if (!geo.isGeoNumeric()) { return false; } freqValues[rowIndex][colIndex] = (int) ((GeoNumeric) rowGeo .get(colIndex)).getDouble(); rowSum[rowIndex] += freqValues[rowIndex][colIndex]; colSum[colIndex] += freqValues[rowIndex][colIndex]; totalSum += freqValues[rowIndex][colIndex]; } } return true; } /** * Computes expected counts and chi-square contributions */ private void computeChiTestValues() { expected = new double[rowValues.length][colValues.length]; chiCont = new double[rowValues.length][colValues.length]; for (int rowIndex = 0; rowIndex < rowValues.length; rowIndex++) { for (int colIndex = 0; colIndex < colValues.length; colIndex++) { expected[rowIndex][colIndex] = 1.0 * rowSum[rowIndex] * colSum[colIndex] / totalSum; chiCont[rowIndex][colIndex] = (freqValues[rowIndex][colIndex] - expected[rowIndex][colIndex]); chiCont[rowIndex][colIndex] = chiCont[rowIndex][colIndex] * chiCont[rowIndex][colIndex] / expected[rowIndex][colIndex]; } } } @Override public final void compute() { boolean dataLoaded; if (isRawData) { dataLoaded = loadRawDataValues(); } else { dataLoaded = loadPreparedDataValues(); } if (!dataLoaded) { table.setUndefined(); return; } parseArgs(); computeChiTestValues(); tableSb.setLength(0); // prepare array beginTable(); // table header addTableRow(tableSb, -1, handleSpecialChar(getLoc().getMenu("Frequency")), "colValue", lastRow == 0); if (showRowPercent) { addTableRow(tableSb, 0, handleSpecialChar(getLoc().getPlain("RowPercent")), "blank", lastRow == 1); } if (showColPercent) { addTableRow(tableSb, 0, handleSpecialChar(getLoc().getPlain("ColumnPercent")), "blank", lastRow == 2); } if (showTotalPercent) { addTableRow(tableSb, 0, handleSpecialChar(getLoc().getPlain("TotalPercent")), "blank", lastRow == 3); } if (showExpected) { addTableRow(tableSb, 0, handleSpecialChar(getLoc().getPlain("ExpectedCount")), "blank", lastRow == 4); } if (showChi) { addTableRow(tableSb, 0, handleSpecialChar( getLoc().getPlain("ChiSquaredContribution")), "blank", lastRow == 5); } // remaining rows for (int rowIndex = 0; rowIndex < rowValues.length; rowIndex++) { addTableRow(tableSb, rowIndex, rowValues[rowIndex], "count", lastRow == 0); if (showRowPercent) { addTableRow(tableSb, rowIndex, null, "_", lastRow == 1); } if (showColPercent) { addTableRow(tableSb, rowIndex, null, "|", lastRow == 2); } if (showTotalPercent) { addTableRow(tableSb, rowIndex, null, "+", lastRow == 3); } if (showExpected) { addTableRow(tableSb, rowIndex, null, "e", lastRow == 4); } if (showChi) { addTableRow(tableSb, rowIndex, null, "k", lastRow == 5); } } // table footer addTableRow(tableSb, -1, getLoc().getMenu("Total"), "tableFooter", !showRowPercent); if (showRowPercent) { addTableRow(tableSb, 0, null, "rowPercentFooter", true); } endTable(tableSb); if (showTest) { addChiTest(tableSb); } table.setTextString(tableSb.toString()); } private static void endTable(StringBuilder sb2) { sb2.append("\\end{array}"); } private void addChiTest(StringBuilder sb) { AlgoChiSquaredTest test; if (isRawData) { test = new AlgoChiSquaredTest(cons, freq.getResult(), null); } else { test = new AlgoChiSquaredTest(cons, freqMatrix, null); } cons.removeFromConstructionList(test); GeoList result = test.getResult(); String split = "&"; String rowHeader = getLoc().getMenu("DegreesOfFreedom.short") + split + Unicode.chi + Unicode.Superscript_2 + split + getLoc().getMenu("PValue"); String degFreedom = kernel.format( (rowValues.length - 1) * (colValues.length - 1), StringTemplate.numericDefault); String secondRow = degFreedom + split + result.get(1).toValueString(StringTemplate.numericDefault) + split + result.get(0).toValueString(StringTemplate.numericDefault); sb.append("\\\\ \\text{"); sb.append(getLoc().getMenu("ChiSquaredTest")); sb.append("}\\\\"); sb.append("\\begin{array}{|l|l|l|l|}"); sb.append(" \\\\ \\hline "); sb.append(rowHeader); sb.append("\\\\"); sb.append("\\hline "); sb.append(secondRow); sb.append("\\\\"); sb.append("\\hline "); sb.append("\\end{array}"); } private void beginTable() { tableSb.append("\\begin{array}{|l"); for (int i = 0; i < colValues.length - 1; i++) { tableSb.append("|l"); } tableSb.append("|l||l|}"); // extra column for margin tableSb.append(" \\\\ "); } private void addTableRow(StringBuilder sb, int rowIndex, String header, String type, boolean lineBelow) { double x; startRow(sb, rowIndex == -1); // row header if (header == null) { sb.append("\\;"); } else { sb.append(header); } endCell(sb); // row elements for (int colIndex = 0; colIndex < colValues.length; colIndex++) { if ("blank".equals(type)) { sb.append("\\;"); } else if ("colValue".equals(type)) { sb.append(colValues[colIndex]); } else if ("count".equals(type)) { sb.append(freqValues[rowIndex][colIndex]); } else if ("_".equals(type)) { x = 100.0 * freqValues[rowIndex][colIndex] / rowSum[rowIndex]; sb.append(kernel.format(x, StringTemplate.numericDefault)); } else if ("|".equals(type)) { x = 100.0 * freqValues[rowIndex][colIndex] / colSum[colIndex]; sb.append(kernel.format(x, StringTemplate.numericDefault)); } else if ("+".equals(type)) { x = 100.0 * freqValues[rowIndex][colIndex] / totalSum; sb.append(kernel.format(x, StringTemplate.numericDefault)); } else if ("e".equals(type)) { x = expected[rowIndex][colIndex]; sb.append(kernel.format(x, StringTemplate.numericDefault)); } else if ("k".equals(type)) { x = chiCont[rowIndex][colIndex]; sb.append(kernel.format(x, StringTemplate.numericDefault)); } else if ("tableFooter".equals(type)) { sb.append(colSum[colIndex]); } else if ("rowPercentFooter".equals(type)) { x = 100.0 * colSum[colIndex] / totalSum; sb.append(kernel.format(x, StringTemplate.numericDefault)); } endCell(sb); } // margin if ("count".equals(type)) { sb.append(rowSum[rowIndex]); } else if ("colValue".equals(type)) { sb.append(getLoc().getMenu("Total")); } else if ("|".equals(type)) { x = 100.0 * rowSum[rowIndex] / totalSum; sb.append(kernel.format(x, StringTemplate.numericDefault)); } else if ("tableFooter".equals(type)) { sb.append(totalSum); } else { sb.append("\\;"); } endRow(sb, lineBelow); } private static void startRow(StringBuilder sb, boolean lineAbove) { if (lineAbove) { sb.append("\\hline "); } } private static void endRow(StringBuilder sb, boolean lineBelow) { sb.append("\\\\"); if (lineBelow) { sb.append("\\hline "); } } private static void endCell(StringBuilder sb) { sb.append("&"); } private static String handleSpecialChar(String s) { return s.replaceAll(" ", "\\\\;"); } }