Repository: jtablesaw/tablesaw (java)

Issue:
TableSliceGroup.aggregate is failing when aggregating multiple columns
TableSliceGroup.aggregate is failing when aggregating multiple columns at once.

I added the following test to TableSliceGroupTest to show the issue.

```java
    @Test
    public void aggregateWithMultipleColumns() {
        table.addColumns(table.categoricalColumn("approval").copy().setName("approval2"));
        TableSliceGroup group = StandardTableSliceGroup.create(table, table.categoricalColumn("who"));
        Table aggregated = group.aggregate(ImmutableListMultimap.of("approval", exaggerate, "approval2", exaggerate));

        assertEquals(aggregated.rowCount(), group.size());
    }
```

Is failing with the following error:
```
java.lang.IllegalArgumentException: Column exageration [approval2] does not have the same number of rows as the other columns in the table.
```

Pull request description:
Fix bug in TableSliceGroup.aggregate when aggregating over multiple c…

- [X] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Fixes #557 

TableSliceGroup.aggregate was failing when aggregating over multiple columns.

The slice name was appended to the result table a second time when during the aggregation of the second column. This caused the result column to have to contain twice a many rows and the aggregation to fail.

What was changed
Add the slice name to the result table exactly once.

## Testing

Yes. Added a failing test and then fixed the bug causing the test to pass.

Patch:
diff --git a/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java b/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
index 422ca410..537e0e23 100644
--- a/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
+++ b/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
@@ -153,16 +153,16 @@ public class TableSliceGroup implements Iterable<TableSlice> {
         Table groupTable = summaryTableName(sourceTable);
         StringColumn groupColumn = StringColumn.create("Group");
         groupTable.addColumns(groupColumn);
+        boolean firstFunction = true;
         for (Map.Entry<String, Collection<AggregateFunction<?,?>>> entry : functions.asMap().entrySet()) {
             String columnName = entry.getKey();
-            int functionCount = 0;
             for (AggregateFunction function : entry.getValue()) {
                 String colName = aggregateColumnName(columnName, function.functionName());
                 ColumnType type = function.returnType();
                 Column resultColumn = type.create(colName);
                 for (TableSlice subTable : getSlices()) {
                     Object result = function.summarize(subTable.column(columnName));
-                    if (functionCount == 0) {
+                    if (firstFunction) {
                         groupColumn.append(subTable.name());
                     }
                     if (result instanceof Number) {
@@ -173,7 +173,7 @@ public class TableSliceGroup implements Iterable<TableSlice> {
                     }
                 }
                 groupTable.addColumns(resultColumn);
-                functionCount++;
+                firstFunction = false;
             }
         }
         return splitGroupingColumn(groupTable);
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/QQPlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/QQPlot.java
index ee5b883b..aae3b1f8 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/api/QQPlot.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/QQPlot.java
@@ -75,7 +75,9 @@ public class QQPlot {
 
         Layout layout = Layout.builder()
                 .title(title)
-                .xAxis(Axis.builder().title(xTitle).build())
+                .xAxis(Axis.builder()
+                        .title(xTitle)
+                        .build())
                 .yAxis(Axis.builder().title(yTitle).build())
                 .height(700)
                 .width(900)
@@ -83,10 +85,17 @@ public class QQPlot {
         return new Figure(layout, trace1, trace2);
     }
 
+    /**
+     * Returns a double array, whose values are quantiles from the given source, based on the given size.
+     * The idea is to produce size elements that represent the quantiles of source array
+     * @param source    The array to whose quantiles are calculated
+     * @param size      The size of the array to return
+     */
     private static double[] interpolate(double[] source, int size) {
         double[] interpolatedData = new double[size];
         for (int i = 0; i < size; i++) {
-            interpolatedData[i] = StatUtils.percentile(source, i + 1 /(double) size);
+            double value = ((i + .5) / (double) size) * 100;
+            interpolatedData[i] = StatUtils.percentile(source, value);
         }
         return interpolatedData;
     }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/api/TukeyMeanDifferencePlot.java b/jsplot/src/main/java/tech/tablesaw/plotly/api/TukeyMeanDifferencePlot.java
new file mode 100644
index 00000000..4acd0c90
--- /dev/null
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/api/TukeyMeanDifferencePlot.java
@@ -0,0 +1,121 @@
+package tech.tablesaw.plotly.api;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.math3.stat.StatUtils;
+import tech.tablesaw.api.NumberColumn;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.plotly.components.Axis;
+import tech.tablesaw.plotly.components.Figure;
+import tech.tablesaw.plotly.components.Layout;
+import tech.tablesaw.plotly.traces.ScatterTrace;
+
+import java.util.Arrays;
+
+/**
+ *
+ * A Tukey Mean-Difference Plot (AKA a Bland-Altman plot) is a kind of scatter plot used frequently in medicine,
+ * biology and other fields, is used to visualize the differences between two quantitative measurements. In particular,
+ * it is often used to evaluate whether two tests produce 'the same' result.
+ *
+ * For two numeric arrays, a and b, the plot shows the mean for each pair of observations (a + b) / 2, as well as the
+ * differences between them: a - b.
+ *
+ * For more information:
+ * https://en.wikipedia.org/wiki/Bland%E2%80%93Altman_plot
+ */
+public class TukeyMeanDifferencePlot {
+
+    /**
+     * Returns a figure containing a Tukey Mean-Difference Plot describing the differences between the data in two
+     * columns of interest
+     * @param title         A title for the plot
+     * @param measure       The measure being compared on the plot (e.g "inches" or "height in inches"
+     * @param table         The table containing the columns of interest
+     * @param columnName1   The name of the first numeric column containing the data to plot
+     * @param columnName2   The name of the second numeric column containing the data to plot
+     * @return              A quantile plot
+     */
+    public static Figure create(String title, String measure, Table table, String columnName1, String columnName2) {
+
+        NumberColumn<?> xCol = table.nCol(columnName1);
+        NumberColumn<?> yCol = table.nCol(columnName2);
+
+        return create(title, measure, xCol.asDoubleArray(), yCol.asDoubleArray());
+    }
+
+    /**
+     * Returns a figure containing a QQ Plot describing the differences between the distribution of values
+     * in the columns of interest
+     * @param title    A title for the plot
+     * @param measure  The measure being compared on the plot (e.g "inches" or "height in inches"
+     * @param xData    The data to plot on the x Axis
+     * @param yData    The data to plot on the y Axis
+     * @return         A quantile plot
+     */
+    public static Figure create(String title, String measure, double[] xData, double[] yData) {
+
+        Preconditions.checkArgument(xData.length != 0, "x Data array is empty");
+        Preconditions.checkArgument(yData.length != 0, "x Data array is empty");
+
+        if (xData.length != yData.length) {
+            double[] interpolatedData;
+            if (xData.length < yData.length) {
+                interpolatedData = interpolate(yData, xData.length);
+                yData = interpolatedData;
+            } else {
+                interpolatedData = interpolate(xData, yData.length);
+                xData = interpolatedData;
+            }
+        }
+
+        Arrays.sort(xData);
+        Arrays.sort(yData);
+
+        double[] averagePoints = new double[xData.length];
+        double[] differencePoints = new double[xData.length];
+        for (int i = 0; i < xData.length; i++) {
+            averagePoints[i] = (xData[i] + yData[i]) /2.0;
+            differencePoints[i] = (xData[i] - yData[i]);
+        }
+
+        double xMin = StatUtils.min(xData);
+        double xMax = StatUtils.max(xData);
+        double[] zeroLineX = {xMin, xMax};
+        double[] zeroLineY = {0, 0};
+
+        // Draw the line indicating equal distributions (this is zero in this plot)
+        ScatterTrace trace1 = ScatterTrace.builder(zeroLineX, zeroLineY)
+                .mode(ScatterTrace.Mode.LINE)
+                .name("y = x")
+                .build();
+
+        // Draw the actual data points
+        ScatterTrace trace2 = ScatterTrace.builder(averagePoints, differencePoints)
+                .name("mean x difference")
+                .build();
+
+        Layout layout = Layout.builder()
+                .title(title)
+                .xAxis(Axis.builder().title("mean (" + measure + ")").build())
+                .yAxis(Axis.builder().title("difference (" + measure + ")").build())
+                .height(700)
+                .width(900)
+                .build();
+        return new Figure(layout, trace1, trace2);
+    }
+
+    /**
+     * Returns a double array, whose values are quantiles from the given source, based on the given size.
+     * The idea is to produce size elements that represent the quantiles of source array
+     * @param source    The array to whose quantiles are calculated
+     * @param size      The size of the array to return
+     */
+    private static double[] interpolate(double[] source, int size) {
+        double[] interpolatedData = new double[size];
+        for (int i = 0; i < size; i++) {
+            double value = ((i + .5) / (double) size) * 100;
+            interpolatedData[i] = StatUtils.percentile(source, value);
+        }
+        return interpolatedData;
+    }
+}


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/table/TableSliceGroupTest.java b/core/src/test/java/tech/tablesaw/table/TableSliceGroupTest.java
index 0b7685eb..ac1adb0c 100644
--- a/core/src/test/java/tech/tablesaw/table/TableSliceGroupTest.java
+++ b/core/src/test/java/tech/tablesaw/table/TableSliceGroupTest.java
@@ -17,6 +17,7 @@ package tech.tablesaw.table;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import com.google.common.collect.ImmutableListMultimap;
 import java.util.List;
 
 import org.apache.commons.math3.stat.StatUtils;
@@ -87,4 +88,20 @@ public class TableSliceGroupTest {
         List<Table> tables = group.asTableList();
         assertEquals(6, tables.size());
     }
+
+    @Test
+    public void aggregate() {
+        TableSliceGroup group = StandardTableSliceGroup.create(table, table.categoricalColumn("who"));
+        Table aggregated = group.aggregate("approval", exaggerate);
+        assertEquals(aggregated.rowCount(), group.size());
+    }
+
+    @Test
+    public void aggregateWithMultipleColumns() {
+        table.addColumns(table.categoricalColumn("approval").copy().setName("approval2"));
+        TableSliceGroup group = StandardTableSliceGroup.create(table, table.categoricalColumn("who"));
+
+        Table aggregated = group.aggregate(ImmutableListMultimap.of("approval", exaggerate, "approval2", exaggerate));
+        assertEquals(aggregated.rowCount(), group.size());
+    }
 }
\ No newline at end of file
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/QQPlotExample.java b/jsplot/src/test/java/tech/tablesaw/examples/QQPlotExample.java
index 5f27d914..16d41fcd 100644
--- a/jsplot/src/test/java/tech/tablesaw/examples/QQPlotExample.java
+++ b/jsplot/src/test/java/tech/tablesaw/examples/QQPlotExample.java
@@ -28,9 +28,15 @@ public class QQPlotExample {
         Table baseball = Table.read().csv("../data/baseball.csv");
         Plot.show(QQPlot.create("batting averages and On-base percent", baseball, "BA", "SLG"));
 
-        // example with differen sized arrays;
+        // example with different sized arrays;
         double[] first = new NormalDistribution().sample(100);
         double[] second = new NormalDistribution().sample(200);
         Plot.show(QQPlot.create("Test of different sized arrays", "short array", first, "long array", second));
+
+        // example with different sized arrays;
+        first = new NormalDistribution().sample(20000);
+        second = new NormalDistribution().sample(19990);
+        Plot.show(QQPlot.create("Test of different sized arrays", "long array", first, "short array", second));
+
     }
 }
\ No newline at end of file
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/TukeyMeanDistributionPlotExample.java b/jsplot/src/test/java/tech/tablesaw/examples/TukeyMeanDistributionPlotExample.java
new file mode 100644
index 00000000..619d78a5
--- /dev/null
+++ b/jsplot/src/test/java/tech/tablesaw/examples/TukeyMeanDistributionPlotExample.java
@@ -0,0 +1,40 @@
+/*
+ * 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 tech.tablesaw.examples;
+
+import org.apache.commons.math3.distribution.NormalDistribution;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.plotly.Plot;
+import tech.tablesaw.plotly.api.TukeyMeanDifferencePlot;
+
+/**
+ *  Illustrates how to create a quantile plot for visualizing a distribution
+ */
+public class TukeyMeanDistributionPlotExample {
+
+    public static void main(String[] args) throws Exception {
+        Table baseball = Table.read().csv("../data/baseball.csv");
+        Table nl = baseball.where(baseball.stringColumn("league").isEqualTo("NL"));
+        Table al = baseball.where(baseball.stringColumn("league").isEqualTo("AL"));
+        Plot.show(TukeyMeanDifferencePlot.create("Wins NL vs AL", "wins",
+                nl.intColumn("W").asDoubleArray(),
+                al.intColumn("W").asDoubleArray()));
+
+        // example with difference sized arrays;
+        double[] first = new NormalDistribution().sample(100);
+        double[] second = new NormalDistribution().sample(200);
+        Plot.show(TukeyMeanDifferencePlot.create("Test of different sized arrays", "random data", first, second));
+    }
+}
\ No newline at end of file