/*
* This code is part of Project Waterloo from King's College London
*
*
* Copyright King's College London 2012-
*
* @author Malcolm Lidierth, King's College London [Contact]
*
* Project Waterloo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Project Waterloo 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*
*/
package kcl.waterloo.math;
import java.util.Arrays;
import java.util.Formatter;
/**
* Provides basic statistics for the columns of a double[][] array.
*
* Columns should contain the same number of elements. Missing values should be
* entered as NaNs.
*
* Statistics available are :
*
* Sum
*
* Mean
*
* Variance
*
* Standard deviation
*
* Skew
*
* Kurtosis
*
* Minimum
*
* Maximum
*
* Median
*
* Upper and Lower percentiles
*
* Number of valid (non-NaN) values
* Geometric mean
*
*
* NaNs are treated as missing values and discounted from all calculations.
*
* An internal matrix contains the following parameters in the rows specified by
* ColumnStats.X where X is a static final int.
*
*
* ColumnStats.SUM the sum of values.
*
* ColumnStats.SUMDELTA2 the sum of squares of residuals.
*
* ColumnStats.SUMDELTA3 the sum of cubes of residuals.
* ColumnStats.SUMDELTA4 the sum of residuals to power 4.
* ColumnStats.MIN minimum value.
*
* ColumnStats.MAX maximum value.
*
* ColumnStats.MEDIAN median value.
*
* ColumnStats.LOWER lower percent.
*
* ColumnStats.UPPER upper percent.
*
* ColumnStats.SUMLOG sum of logs.
*
* ColumnStats.N number of observations (discounting NaNs).
*
* ColumnStats.Pct the requested percent (0 to 50):
*
* LOWER contains these values while UPPER contains values for percent =
* (100-ColumnStats.Pct).
*
*
*
* @author Malcolm Lidierth, King's College London [Contact]
*/
public class ColumnStats {
/**
* An internal buffer for intermediate results. Contents of the columns are
* as defined by the names of the static int fields below.
*/
private final double[][] summary;
/**
*
*/
public static final int SUM = 0, SUMDELTA2 = 1, SUMDELTA3 = 2, SUMDELTA4 = 3,
MIN = 4, MAX = 5, MEDIAN = 6,
LOWER = 7, UPPER = 8, SUMLOG = 9, N = 10;
/**
* When true, variance will be corrected by (N-1) instead of N. Default is
* true.
*/
private boolean varianceCorrected = true;
/**
* When true, the skew will be adjusted by multiplying by by Math.sqrt(N *
* (N - 1)) / (N - 2). Default is false.
*/
private boolean skewCorrected = false;
/**
* When true, the kurtosis will be adjusted as ((N+1) * unadjusted kurtosis
* - 3*(N-1)) * (N-1) /((N-2)*(N-3)). Default is false.
*/
private boolean kurtosisCorrected = false;
/**
*
*/
private final int percent;
/**
* Returns statistics using 25th/75th percentiles
*
* @param data the data to analyze
*/
public ColumnStats(double[][] data) {
this(data, 25);
}
/**
* Returns statistics using specified percentiles
*
* @param data the data to analyze
* @param percentile the percentiles to use
*/
public ColumnStats(double[][] data, int percentile) {
summary = initialMatrix(data[0].length);
this.percent = percentile;
createSummary(data);
}
private static double[][] initialMatrix(int N) {
double[][] matrix = new double[11][N];
ArrayMath.filli(matrix[MIN], Double.POSITIVE_INFINITY);
ArrayMath.filli(matrix[MAX], Double.NEGATIVE_INFINITY);
return matrix;
}
/**
* Returns the number of columns for the input array.
*
* @return number of columns
*/
public int getDimension() {
return summary[0].length;
}
/**
* Returns the sum of the data in each column of the input.
*
* @return the sum
*/
public double[][] getSum() {
return new double[][]{summary[SUM].clone()};
}
/**
* Returns the mean of the data in each column of the input.
*
* @return the mean
*/
public double[][] getMean() {
return new double[][]{ArrayMath.div(summary[SUM], summary[N])};
}
/**
* Returns the variance of the data in each column of the input. Bias
* correction is applied if {@code varianeCorrected} is true.
*
* @return the variance
*/
public double[][] getVariance() {
double[] factor;
if (varianceCorrected) {
factor = ArrayMath.div(ArrayMath.filli(new double[getDimension()], 1d), ArrayMath.sub(summary[N], 1));
} else {
factor = ArrayMath.div(ArrayMath.filli(new double[getDimension()], 1d), summary[N]);
}
return new double[][]{ArrayMath.muli(factor, summary[SUMDELTA2])};
}
/**
* Returns the standard deviation of the data in each column of the input.
* Bias correction is applied to the variance if {@code varianceCorrected}
* is true.
*
* @return the standard deviation
*/
public double[][] getSD() {
return new double[][]{ArrayMath.sqrt(getVariance()[0])};
}
/**
* Returns the skew of the data in each column of the input. Bias correction
* is applied to the variance if {@code skewCorrected} is true.
*
* @return the skew
*/
public double[][] getSkew() {
double[] NN = getN()[0];
double[] num = ArrayMath.div(summary[SUMDELTA3], NN);
// Uncorrected SD
boolean old = varianceCorrected;
varianceCorrected = false;
double[] den = getSD()[0];
varianceCorrected = old;
den = ArrayMath.powi(den, 3);
double[] result = ArrayMath.div(num, den);
if (skewCorrected) {
for (int k = 0; k < NN.length; k++) {
if (NN[k] >= 3) {
result[k] *= Math.sqrt(NN[k] * (NN[k] - 1)) / (NN[k] - 2);
}
}
}
return new double[][]{result};
}
/**
* Returns the kurtosis of the data in each column of the input. Bias
* correction is applied to the variance if {@code kurtosisCorrected} is
* true.
*
* @return the kurtosis
*/
public double[][] getKurtosis() {
double[] NN = getN()[0];
double[] num = ArrayMath.div(summary[SUMDELTA4], NN);
// Uncorrected SD
boolean old = varianceCorrected;
varianceCorrected = false;
double[] den = getVariance()[0];
varianceCorrected = old;
den = ArrayMath.powi(den, 2);
double[] result = ArrayMath.div(num, den);
if (kurtosisCorrected) {
for (int k = 0; k < NN.length; k++) {
if (NN[k] >= 4) {
result[k] = ((NN[k] + 1) * result[k] - 3 * (NN[k] - 1))
* (NN[k] - 1) / ((NN[k] - 2) * (NN[k] - 3)) + 3;
}
}
}
return new double[][]{result};
}
/**
* Returns the minimum value for each column of the data.
*
* @return the minimum
*/
public double[][] getMinimum() {
return new double[][]{summary[MIN].clone()};
}
/**
* Returns the maximum value for each column of the data.
*
* @return the maximum
*/
public double[][] getMaximum() {
return new double[][]{summary[MAX].clone()};
}
/**
* Returns the geometric mean for each column of the data.
*
* @return the geometric mean
*/
public double[][] getGeometricMean() {
double[] g = ArrayMath.div(summary[SUMLOG], summary[N]);
for (int k = 0; k < g.length; k++) {
g[k] = Math.exp(g[k]);
}
return new double[][]{g};
}
/**
* Returns the number of data points for each column of the data. NaNs are
* excluded.
*
* @return the number of data points
*/
public double[][] getN() {
return new double[][]{summary[N].clone()};
}
/**
* Returns the median value for each column of the data.
*
* @return the median
*/
public double[][] getMedian() {
return new double[][]{summary[MEDIAN].clone()};
}
/**
* Returns the lower pecentile for each column of the data.
*
* @return the lower pecentile
*/
public double[][] getLower() {
return new double[][]{summary[LOWER].clone()};
}
/**
* Returns the upper pecentile for each column of the data.
*
* @return the upper pecentile
*/
public double[][] getUpper() {
return new double[][]{summary[UPPER].clone()};
}
/**
* Returns the lower pecentile used. The upper percentile is (100-lower
* percentile)
*
* @return the pecentile
*/
public double getPercentile() {
return percent;
}
/**
* Returns a copy of the intermediate array used in calculations.
*
* @return a double[][]
*/
public double[][] getSummary() {
return summary.clone();
}
/**
* Returns true if bias correction is to be applied to the variance
*
* @return the correction flag
*/
public boolean isVarianceCorrected() {
return varianceCorrected;
}
/**
* Sets bias correction mode for the variance (and standard deviation)
*
* @param flag
*/
public void setVarianceCorrected(boolean flag) {
varianceCorrected = flag;
}
/**
* Returns true if bias correction is to be applied to the skew
*
* @return the correction flag
*/
public boolean isSkewCorrected() {
return skewCorrected;
}
/**
* Sets bias correction mode for the skew
*
* @param flag
*/
public void setSkewCorrected(boolean flag) {
skewCorrected = flag;
}
/**
* Returns true if bias correction is to be applied to the kurtosis
*
* @return the correction flag
*/
public boolean isKurtosisCorrected() {
return kurtosisCorrected;
}
/**
* Sets bias correction mode for the kurtosis
*
* @param flag
*/
public void setKurtosisCorrected(boolean flag) {
kurtosisCorrected = flag;
}
/**
* Called by the constructor.
*
* @param data
* @return a double[][]
*/
private double[][] createSummary(double[][] data) {
if (percent < 0d || percent > 50d) {
return new double[0][0];
}
double[][] tr = new double[data[0].length][data.length];
// Pass 1: Search through rows of the input to find min/max and N
// creating a tranposed copy on the way
for (int k = 0; k < data.length; k++) {
for (int kk = 0; kk < data[k].length; kk++) {
if (!Double.isNaN(data[k][kk])) {
summary[SUMLOG][kk] += Math.log(data[k][kk]);
summary[MIN][kk] = Math.min(summary[MIN][kk], data[k][kk]);
summary[MAX][kk] = Math.max(summary[MAX][kk], data[k][kk]);
summary[N][kk]++;
}
// Create transposed data array - include NaNs
tr[kk][k] = data[k][kk];
}
}
// Find the sums of (x-xhat)^2 long-hand
for (int m = 0; m < data[0].length; m++) {
summary[SUM][m] = localSum(tr[m]);
double mean = summary[SUM][m] / summary[N][m];
double[] residuals = ArrayMath.sub(tr[m], mean);
double[] raised = ArrayMath.pow(residuals, 2d);//.^2
summary[SUMDELTA2][m] = localSum(raised);
raised = ArrayMath.pow(residuals, 3d);//.^3
summary[SUMDELTA3][m] = localSum(raised);
raised = ArrayMath.pow(residuals, 4d);//.^4
summary[SUMDELTA4][m] = localSum(raised);
}
for (int n = 0; n < tr.length; n++) {
// Sort tr in-place: -Inf at start, +Inf then NaN at end
Arrays.sort(tr[n]);
// Find median and percentiles - use N from above and thus ignore
// NaN entries
summary[MEDIAN][n] = median(tr[n], (int) summary[N][n]);
summary[LOWER][n] = percentile(tr[n], (int) summary[N][n], percent);
summary[UPPER][n] = percentile(tr[n], (int) summary[N][n], 100 - percent);
}
for (int k = 0; k < summary.length; k++) {
for (int kk = 0; kk < summary[k].length; kk++) {
if (summary[N][kk] == 0) {
summary[SUM][kk] = Double.NaN;
summary[MIN][kk] = Double.NaN;
summary[MAX][kk] = Double.NaN;
}
}
}
return summary;
}
/**
* Compensated sum
*
* @param in
* @return a double
*/
private static double localSum(double[] in) {
double sum = 0d;
double x, y = 0d, z;
for (int k = 0; k < in.length; k++) {
x = sum + in[k];
z = x - sum;
y += ((sum - (x - z)) + (in[k] - z));
sum = x;
}
return sum + y;
}
private static double median(final double[] in, int N) {
if (N == 0) {
return Double.NaN;
}
if (N == 1) {
return in[0];
}
if (N % 2 != 0) {
return in[N / 2];
} else {
return (in[N / 2 - 1] + in[N / 2]) / 2;
}
}
/**
* Percentile from linear interpolation between nearest ranks.
*
* See here for details.
*
*
*/
private static double percentile(final double[] in, int N, int percentile) {
if (N == 1) {
return in[0];
}
double pos = percentile * (N + 1) / 100d;
if (pos < 1) {
return in[0];
} else if (pos >= N) {
return in[N - 1];
} else {
double f = Math.floor(pos);
double d = pos - f;
int idx = (int) f - 1;
double result = in[idx] + d * (in[idx + 1] - in[idx]);
if (Double.isNaN(result)) {
if (Double.isInfinite(in[idx])) {
return in[idx];
} else if (Double.isInfinite(in[idx + 1])) {
return in[idx + 1];
}
}
return result;
}
}
// public static double BVar(double[] in) {
// BigDecimal mean = BigDecimal.ZERO;
// for (int k = 0; k < in.length; k++) {
// mean = mean.add(new BigDecimal(in[k]));
// }
// mean = mean.divide(new BigDecimal(in.length));
// BigDecimal var = BigDecimal.ZERO;
// BigDecimal diff;
// for (int k = 0; k < in.length; k++) {
// diff = new BigDecimal(in[k]).subtract(mean);
// var = var.add(diff.multiply(diff));
// }
// BigDecimal factor = new BigDecimal((double) in.length - 1);
// return var.divide(factor, MathContext.DECIMAL128).doubleValue();
// }
@Override
public String toString() {
StringBuilder s = new StringBuilder();
Formatter f = new Formatter(s);
f.format("Summary statistics:%n");
for (int k = 0; k < summary[0].length; k++) {
f.format("\t [" + Integer.toString(k) + "]\t");
}
f.format("%nMean:");
double[][] mean = getMean();
for (int k = 0; k < summary[0].length; k++) {
f.format("\t%10.6f", mean[0][k]);
}
f.format("%nSD:");
if (varianceCorrected) {
f.format("%3s", "[+]");
}
double[][] sd = getSD();
for (int k = 0; k < summary[0].length; k++) {
f.format("\t%10.6f", sd[0][k]);
}
f.format("%nSkew:");
if (skewCorrected) {
f.format("[+]");
}
double[][] skew = getSkew();
for (int k = 0; k < summary[0].length; k++) {
f.format("\t%10.6f", skew[0][k]);
}
f.format("%nK'osis:");
if (kurtosisCorrected) {
f.format("[+]");
}
double[][] kurtosis = getKurtosis();
for (int k = 0; k < summary[0].length; k++) {
f.format("\t%10.6f", kurtosis[0][k]);
}
f.format("%nMin:");
double[][] min = getMinimum();
for (int k = 0; k < summary[0].length; k++) {
f.format("\t%10.6f", min[0][k]);
}
f.format("%nMax:");
double[][] max = getMaximum();
for (int k = 0; k < summary[0].length; k++) {
f.format("\t%10.6f", max[0][k]);
}
f.format("%nN:");
double[][] n = getN();
for (int k = 0; k < summary[0].length; k++) {
f.format("\t%10d", (int) n[0][k]);
}
f.format("%nPercentiles:");
f.format("%20s", "%nMedian:");
double[][] median = getMedian();
for (int k = 0; k < summary[0].length; k++) {
f.format("\t%10.6f", median[0][k]);
}
f.format("%n%2d%%:", percent);
double[][] lower = getLower();
for (int k = 0; k < summary[0].length; k++) {
f.format("\t%10.6f", lower[0][k]);
}
f.format("%n%2d%%:", 100 - percent);
double[][] upper = getUpper();
for (int k = 0; k < summary[0].length; k++) {
f.format("\t%10.6f", upper[0][k]);
}
f.format("%n%n [+] indicates bias correction applied");
return f.toString();
}
}
Ads help maintain this website.