org.opensha.commons.util.DataUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.opensha.commons.util.DataUtils.java

Source

/*******************************************************************************
 * Copyright 2009 OpenSHA.org in partnership with
 * the Southern California Earthquake Center (SCEC, http://www.scec.org)
 * at the University of Southern California and the UnitedStates Geological
 * Survey (USGS; http://www.usgs.gov)
 * 
 * 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 org.opensha.commons.util;

import static com.google.common.base.Preconditions.*;
import static java.lang.Double.*;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;

import org.apache.commons.math3.stat.StatUtils;
import org.opensha.commons.data.function.DefaultXY_DataSet;

import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Range;
import com.google.common.primitives.Doubles;
import com.google.common.primitives.Ints;

/**
 * Utilities for operating on {@code double}-valued data. This class: <ul>
 * <li>should probably be enhanced to work with {@code Double}
 * {@code Collection}s</li> <li>could be renamed to DoubleUtils or something
 * funny like Dubbles</li> </p>
 * 
 * <p>See {@link Doubles}  minimum, maximum, sum,
 * mean, product, etc... of {@code double} arrays as well as other
 * properties</p>
 * 
 * <p>Transformations of {@code double} arrays or {@code List}s may be
 * performed on empty data sets; {@code null} data sets throw an exception.</p>
 * 
 * <p>Class designed to reduce data copying hence why List variants do not
 * call toArray() and delegate to varargs variant.</p>
 * 
 * @author Peter Powers
 * @author Kevin Milner
 * @version $Id: DataUtils.java 11112 2015-10-08 16:44:01Z pmpowers $
 */
public class DataUtils {

    private DataUtils() {
    }

    /*
     * Some quick tests of abs() and scale() using a non-Function based approach
     * and hence no autoboxing showed only marginal slowdown over a 10^8 sized
     * array of random values. If profiling shows that in practice the function
     * based approach of transforming arrays is slow, primitive implementations
     * may be substituted. See DubblesTest for speed test.
     * 
     * Similarly, boolean tests such as isSorted() could be short-circuited to
     * return at the first failure. However, there is more reusable code in the
     * current implementation that is easier to follow. Again, this will may be
     * changed if there is a demonstrable performance hit.
     * 
     * We could probably intern commonly used scale functions.
     */

    private static final Range<Double> POS_RANGE = Range.open(0d, Double.POSITIVE_INFINITY);

    /**
     * Returns the difference between {@code test} and {@code target}, relative
     * to {@code target}, as a percent. If {@code target} is 0, method returns 0
     * if {@code test} is also 0, otherwise {@code Double.POSITIVE_INFINITY}. If
     * either value is {@code Double.NaN}, method returns {@code Double.NaN}.
     * @param test value
     * @param target value
     * @return the percent difference
     */
    public static double getPercentDiff(double test, double target) {
        if (isNaN(target) || isNaN(test))
            return NaN;
        if (target == 0)
            return test == 0 ? 0 : POSITIVE_INFINITY;
        return Math.abs(test - target) / target * 100d;
    }

    /**
     * Returns whether the supplied {@code data} are all positive.
     * @param data to check
     * @return {@code true} if all values are &ge;0
     */
    public static boolean isPositive(double... data) {
        checkNotNull(data);
        checkArgument(data.length > 0, "data is empty");
        for (double d : data) {
            if (d >= 0)
                continue;
            return false;
        }
        return true;
    }

    /**
     * Returns whether the supplied {@code data} are all positive.
     * @param data to check
     * @return {@code true} if all values are &ge;0
     */
    public static boolean isPositive(List<Double> data) {
        checkNotNull(data);
        checkArgument(data.size() > 0, "data is empty");
        for (double d : data) {
            if (d >= 0)
                continue;
            return false;
        }
        return true;
    }

    /**
     * Returns whether the elements of the supplied {@code data} increase or
     * decrease monotonically, with a flag indicating if duplicate elements are
     * permitted. The {@code repeats} flag could be {@code false} if checking
     * the x-values of a function for any steps, or {@code true} if checking the
     * y-values of a cumulative distribution function, which are commonly
     * constant.
     * @param ascending if {@code true}, descending if {@code false}
     * @param repeats whether repeated adjacent elements are allowed
     * @param data to validate
     * @return {@code true} if monotonic, {@code false} otherwise
     */
    public static boolean isMonotonic(boolean ascending, boolean repeats, double... data) {
        double[] diff = diff(data);
        if (!ascending)
            flip(diff);
        double min = Doubles.min(diff);
        return (repeats) ? min >= 0 : min > 0;
    }

    /**
     * Returns the difference of adjacent elements in the supplied {@code data}.
     * Method returns results in a new array that has {@code data.length - 1}
     * where differences are computed per {@code data[i+1] - data[i]}.
     * @param data to difference
     * @return the differences between adjacent values
     */
    public static double[] diff(double... data) {
        checkNotNull(data);
        checkArgument(data.length > 1);
        int size = data.length - 1;
        double[] diff = new double[size];
        for (int i = 0; i < size; i++) {
            diff[i] = data[i + 1] - data[i];
        }
        return diff;
    }

    /**
     * Creates a sequence of values starting at {@code min} and ending at
     * {@code max}, the log of which are evenly spaced.
     * @param min sequence value
     * @param max sequence value
     * @param step sequence spacing
     * @param ascending if {@code true}, descending if {@code false}
     * @return a monotonically increasing or decreasing sequence where the log
     *         of the values are evenly spaced
     * @throws IllegalArgumentException if {@code min >= max}, {@code step <= 0}
     *         , or any arguments are {@code Double.NaN},
     *         {@code Double.POSITIVE_INFINITY}, or
     *         {@code Double.NEGATIVE_INFINITY}
     * 
     */
    public static double[] buildLogSequence(double min, double max, double step, boolean ascending) {
        double[] seq = buildSequence(Math.log(min), Math.log(max), Math.log(step), ascending);
        return exp(seq);
    }

    /**
     * Creates a sequence of evenly spaced values starting at {@code min} and
     * ending at {@code max}. If {@code (max - min) / step} is not integer
     * valued, the last step in the sequence will be {@code &lt;step}.
     * @param min sequence value
     * @param max sequence value
     * @param step sequence spacing
     * @param ascending if {@code true}, descending if {@code false}
     * @return a monotonically increasing or decreasing sequence of values
     * @throws IllegalArgumentException if {@code min >= max}, {@code step <= 0}
     *         , or any arguments are {@code Double.NaN},
     *         {@code Double.POSITIVE_INFINITY}, or
     *         {@code Double.NEGATIVE_INFINITY}
     */
    public static double[] buildSequence(double min, double max, double step, boolean ascending) {
        // if passed in arguments are NaN, +Inf, or -Inf, and step <= 0,
        // then capacity [c] will end up 0 because (int) NaN = 0, or outside the
        // range 1:10000
        checkArgument(min <= max, "min-max reversed");
        int c = (int) Math.floor((max - min) / step);
        checkArgument(c > 0 && c < MAX_SEQ_LEN, "sequence size");
        if (ascending)
            return buildSequence(min, max, step, c + 2);
        double[] descSeq = buildSequence(-max, -min, step, c + 2);
        return flip(descSeq);
    }

    private static final int MAX_SEQ_LEN = 10001;

    private static double[] buildSequence(double min, double max, double step, int capacity) {
        List<Double> seq = Lists.newArrayListWithCapacity(capacity);
        for (double val = min; val < max; val += step) {
            seq.add(val);
        }
        // TODO given the val < max check above, max will never be reached, therefore
        // we could simply blindly add max and skip the check below
        if (seq.get(seq.size() - 1) != max)
            seq.add(max);
        return Doubles.toArray(seq);
    }

    /**
     * Scales (multiplies) the elements of the supplied {@code data} in place
     * by {@code value}.
     * 
     * <p><b>Note:</b> This method does not check for over/underflow.</p>
     * @param data to scale
     * @param value to scale by
     * @return a reference to the supplied data
     */
    public static double[] scale(double value, double... data) {
        return transform(new Scale(value), data);
    }

    /**
     * Scales (multiplies) the elements of the supplied {@code List} in place
     * by {@code value}.
     * 
     * <p><b>Note:</b> This method does not check for over/underflow.</p>
     * @param list of {@code Double}s to scale
     * @param value to scale by
     * @return a reference to the supplied data
     */
    public static List<Double> scale(double value, List<Double> list) {
        return transform(new Scale(value), list);
    }

    /**
     * Adds the {@code value} to the supplied {@code data} in place.
     * 
     * <p><b>Note:</b> This method does not check for over/underrun.</p>
     * @param data to add to
     * @param value to add
     * @return a reference to the supplied data
     */
    public static double[] add(double value, double... data) {
        return transform(new Add(value), data);
    }

    /**
     * Adds the values of {@code data2} to {@code data1} and returns a reference
     * to {@code data1}.
     * @param data1 
     * @param data2
     * @return a reference to {@code data1}
     */
    public static double[] add(double[] data1, double[] data2) {
        checkArgument(checkNotNull(data1).length == checkNotNull(data2).length);
        for (int i = 0; i < data1.length; i++) {
            data1[i] += data2[i];
        }
        return data1;
    }

    /**
     * Adds the values of {@code data2} to {@code data1} and returns a reference
     * to {@code data1}.
     * @param data1 
     * @param data2
     * @return a reference to {@code data1}
     */
    public static List<Double> add(List<Double> data1, List<Double> data2) {
        checkArgument(checkNotNull(data1).size() == checkNotNull(data2).size());
        for (int i = 0; i < data1.size(); i++) {
            data1.set(i, data1.get(i) + data2.get(i));
        }
        return data1;
    }

    /**
     * Subtracts the values of {@code data2} from {@code data1} and returns a
     * reference to {@code data1}.
     * @param data1
     * @param data2
     * @return a reference to {@code data1}
     */
    public static double[] subtract(double[] data1, double[] data2) {
        checkArgument(checkNotNull(data1).length == checkNotNull(data2).length);
        for (int i = 0; i < data1.length; i++) {
            data1[i] -= data2[i];
        }
        return data1;
    }

    /**
     * Subtracts the values of {@code data2} from {@code data1} and returns a
     * reference to {@code data1}.
     * @param data1
     * @param data2
     * @return a reference to {@code data1}
     */
    public static List<Double> subtract(List<Double> data1, List<Double> data2) {
        checkArgument(checkNotNull(data1).size() == checkNotNull(data2).size());
        for (int i = 0; i < data1.size(); i++) {
            data1.set(i, data1.get(i) - data2.get(i));
        }
        return data1;
    }

    /**
     * Subtracts the values of {@code data2} from {@code data1} and returns a
     * reference to {@code data1}.
     * @param data1
     * @param data2
     * @return a reference to {@code data1}
     */
    public static double[] multiply(double[] data1, double[] data2) {
        checkArgument(checkNotNull(data1).length == checkNotNull(data2).length);
        for (int i = 0; i < data1.length; i++) {
            data1[i] *= data2[i];
        }
        return data1;
    }

    /**
     * Sets every element of the supplied {@code data} to its absolute value.
     * @param data to operate on
     * @return a reference to the data
     */
    public static double[] abs(double... data) {
        return transform(ABS, data);
    }

    /**
     * Applies the exponential function to every element of the supplied 
     * {@code data}.
     * 
     * <p><b>Note:</b> This method does not check for over/underflow.</p>
     * @param data to operate on
     * @return a reference to the data
     */
    public static double[] exp(double... data) {
        return transform(EXP, data);
    }

    /**
     * Applies the natural log function to every element of the supplied 
     * {@code data}.
     * 
     * @param data to operate on
     * @return a reference to the data
     */
    public static double[] ln(double... data) {
        return transform(LN, data);
    }

    /**
     * Applies the base-10 log function to every element of the supplied 
     * {@code data}.
     * 
     * @param data to operate on
     * @return a reference to the data
     */
    public static double[] log(double... data) {
        return transform(LOG, data);
    }

    /**
     * Flips the sign of every element in the supplied {@code data}.
     * @param data to operate on
     * @return a reference to the data
     */
    public static double[] flip(double... data) {
        return transform(new Scale(-1), data);
    }

    /**
     * Returns the minimum of the supplied values. Method delegates to
     * {@link Doubles#min(double...)}. Method returns {@code Double.NaN} if
     * {@code data} contains {@code Double.NaN}.
     * 
     * @param data array to search
     * @return the minimum of the supplied values
     * @throws IllegalArgumentException if {@code data} is empty
     * @see Doubles#min(double...)
     */
    public static double min(double... data) {
        return Doubles.min(data);
    }

    /**
     * Returns the maximum of the supplied values. Method delegates to
     * {@link Doubles#max(double...)}. Method returns {@code Double.NaN} if
     * {@code data} contains {@code Double.NaN}.
     * 
     * @param data array to search
     * @return the maximum of the supplied values
     * @throws IllegalArgumentException if {@code data} is empty
     * @see Doubles#max(double...)
     */
    public static double max(double... data) {
        return Doubles.max(data);
    }

    /**
     * Returns the sum of the supplied values. Method returns {@code Double.NaN}
     * if {@code data} contains {@code Double.NaN}.
     * 
     * <p><b>Note:</b> This method does not check for over/underflow.</p>
     * @param data to add together
     * @return the sum of the supplied values
     */
    public static double sum(double... data) {
        checkNotNull(data);
        double sum = 0;
        for (double d : data) {
            sum += d;
        }
        return sum;
    }

    /**
     * Returns the sum of the supplied values. Method returns {@code Double.NaN}
     * if {@code data} contains {@code Double.NaN}.
     * 
     * <p><b>Note:</b> This method does not check for over/underflow.</p>
     * @param data to add together
     * @return the sum of the supplied values
     */
    public static double sum(List<Double> data) {
        checkNotNull(data);
        double sum = 0;
        for (double d : data) {
            sum += d;
        }
        return sum;
    }

    public static void main(String[] args) {
        //      double infTest = 1d/0;
        //      System.out.println(infTest);
        //      System.out.println(sum(new double[] {0,Double.NaN,2,3,4,5}));
        //      System.out.println("===");
        //      System.out.println(Double.POSITIVE_INFINITY + 3);
        //      System.out.println(Double.NEGATIVE_INFINITY < 0);
        //      System.out.println("===");
        //      Range<Double> range = Range.open(0d, Double.POSITIVE_INFINITY);
        //      System.out.println(range.contains(Double.POSITIVE_INFINITY));

        double[] vals = new double[] { 4, 1e-6, -3.0, 8d, 0.0, 1e8, Double.POSITIVE_INFINITY };
        List<Double> data = Doubles.asList(vals);
        List<Integer> indices = sortedIndices(data, true);
        System.out.println(indices);

    }

    /**
     * Converts the elements of {@code data} to weights, in place, such that
     * they sum to 1.
     * @param data to convert
     * @return a reference to the supplied array
     * @throws IllegalArgumentException if {@code data} is empty, contains any
     *         {@code Double.NaN} or negative values, or sums to a value outside
     *         the range {@code (0..Double.POSITIVE_INFINITY)}
     */
    public static double[] asWeights(double... data) {
        checkArgument(isPositive(data));
        double sum = sum(data);
        checkArgument(POS_RANGE.contains(sum));
        double scale = 1d / sum;
        return scale(scale, data);
    }

    /**
     * Converts the elements of {@code data} to weights, in place, such that
     * they sum to 1.
     * @param data to convert
     * @return a reference to the supplied array
     * @throws IllegalArgumentException if {@code data} is empty, contains any
     *         {@code Double.NaN} or negative values, or sums to a value outside
     *         the range {@code (0..Double.POSITIVE_INFINITY)}
     */
    public static List<Double> asWeights(List<Double> data) {
        checkArgument(isPositive(data));
        double sum = sum(data);
        checkArgument(POS_RANGE.contains(sum));
        double scale = 1d / sum;
        return scale(scale, data);
    }

    /**
     * Transforms the supplied {@code data} in place as per the supplied
     * {@code function}'s {@link Function#apply(Object)} method.
     * @param function to apply to data elements
     * @param data to operate on
     * @return a reference to the supplied {@code data} array
     */
    private static double[] transform(Function<Double, Double> function, double... data) {
        checkNotNull(data);
        for (int i = 0; i < data.length; i++) {
            data[i] = function.apply(data[i]);
        }
        return data;
    }

    /**
     * Transforms the supplied {@code data} in place as per the supplied
     * {@code function}'s {@link Function#apply(Object)} method.
     * @param function to apply to data elements
     * @param data to operate on
     * @return a reference to the supplied {@code data} array
     */
    private static List<Double> transform(Function<Double, Double> function, List<Double> data) {
        checkNotNull(data);
        for (int i = 0; i < data.size(); i++) {
            data.set(i, function.apply(data.get(i)));
        }
        return data;
    }

    // @formatter:off

    private static final Function<Double, Double> ABS = new Function<Double, Double>() {
        @Override
        public Double apply(Double in) {
            return Math.abs(in);
        }
    };

    private static final Function<Double, Double> EXP = new Function<Double, Double>() {
        @Override
        public Double apply(Double in) {
            return Math.exp(in);
        }
    };

    private static final Function<Double, Double> LN = new Function<Double, Double>() {
        @Override
        public Double apply(Double in) {
            return Math.log(in);
        }
    };

    private static final Function<Double, Double> LOG = new Function<Double, Double>() {
        @Override
        public Double apply(Double in) {
            return Math.log10(in);
        }
    };

    private static class Scale implements Function<Double, Double> {
        private final double scale;

        private Scale(final double scale) {
            this.scale = scale;
        }

        @Override
        public Double apply(Double d) {
            return d * scale;
        }
    }

    private static class Add implements Function<Double, Double> {
        private final double term;

        private Add(final double term) {
            this.term = term;
        }

        @Override
        public Double apply(Double d) {
            return d + term;
        }
    }

    // @formatter:on

    /**
     * Validates the domain of a {@code double} data set. Method verifies
     * that data values all fall between {@code min} and {@code max} range
     * (inclusive). Empty arrays are ignored. If {@code min} is
     * {@code Double.NaN}, no lower limit is imposed; the same holds true
     * for {@code max}. {@code Double.NaN} values in {@code array}
     * will validate.
     * 
     * @param min minimum range value
     * @param max maximum range value
     * @param array to validate
     * @throws IllegalArgumentException if {@code min > max}
     * @throws IllegalArgumentException if any {@code array} value is out of
     *         range
     */
    public final static void validate(double min, double max, double... array) {
        checkNotNull(array, "array");
        for (int i = 0; i < array.length; i++) {
            validate(min, max, array[i]);
        }
    }

    /**
     * Verifies that a {@code double} data value falls within a specified
     * minimum and maximum range (inclusive). If {@code min} is 
     * {@code Double.NaN}, no lower limit is imposed; the same holds true
     * for {@code max}. A value of {@code Double.NaN} will always
     * validate.
     * 
     * @param min minimum range value
     * @param max minimum range value
     * @param value to check
     * @throws IllegalArgumentException if {@code min > max}
     * @throws IllegalArgumentException if value is out of range
     */
    public final static void validate(double min, double max, double value) {
        boolean valNaN = isNaN(value);
        boolean minNaN = isNaN(min);
        boolean maxNaN = isNaN(max);
        boolean both = minNaN && maxNaN;
        boolean neither = !(minNaN || maxNaN);
        if (neither)
            checkArgument(min <= max, "min-max reversed");
        boolean expression = valNaN || both ? true
                : minNaN ? value <= max : maxNaN ? value >= min : value >= min && value <= max;
        checkArgument(expression, "Value [%s] out of range [%s ,%s]", value, min, max);
    }

    /**
     * Creates a new array from the values in a source array at the specified
     * indices. Returned array is of same type as source.
     * 
     * @param array array source
     * @param indices index values of items to select
     * @return a new array of values at indices in source
     * @throws NullPointerException if {@code array} or
     *         {@code indices} are {@code null}
     * @throws IllegalArgumentException if data object is not an array or if
     *         data array is empty
     * @throws IndexOutOfBoundsException if any indices are out of range
     */
    public static Object arraySelect(Object array, int[] indices) {
        checkNotNull(array, "Supplied data array is null");
        checkNotNull(indices, "Supplied index array is null");
        checkArgument(array.getClass().isArray(), "Data object supplied is not an array");
        int arraySize = Array.getLength(array);
        checkArgument(arraySize != 0, "Supplied data array is empty");

        // validate indices
        for (int i = 0; i < indices.length; i++) {
            checkPositionIndex(indices[i], arraySize, "Supplied index");
        }

        Class<? extends Object> srcClass = array.getClass().getComponentType();
        Object out = Array.newInstance(srcClass, indices.length);
        for (int i = 0; i < indices.length; i++) {
            Array.set(out, i, Array.get(array, indices[i]));
        }
        return out;
    }

    /**
     * Sorts the supplied data array in place and returns an {@code int[]}
     * array of the original indices of the data values. For example, if the
     * supplied array is [3, 1, 8], the supplied array will be sorted to [1, 3,
     * 8] and the array [2, 1, 3] will be returned.
     * 
     * @param data array to sort
     * @return the inidices of the unsorted array values
     * @throws NullPointerException if source array is {@code null}
     */
    @Deprecated
    public static int[] indexAndSort(final double[] data) {
        checkNotNull(data, "Source array is null");
        List<Integer> indices = Ints.asList(new int[data.length]);
        for (int i = 0; i < indices.size(); i++) {
            indices.set(i, i);
        }
        Collections.sort(indices, new Comparator<Integer>() {
            @Override
            public int compare(Integer i1, Integer i2) {
                double d1 = data[i1];
                double d2 = data[i2];
                return (d1 < d2) ? -1 : (d1 == d2) ? 0 : 1;
            }
        });
        Arrays.sort(data);
        return Ints.toArray(indices);
    }

    /**
     * Returns an index {@code List} that provides a pointers to sorted
     * {@code data}. Let's say you have a number of {@code List<Double>}s and
     * want to sort them all according to one of your choosing. Supply this
     * method with the desired {@code data} and use the returned indices view
     * any of your arrays according to the sort order of the supplied
     * {@code data}.
     * 
     * <p> <b>Notes:</b> <ul> <li>The supplied data should not be sorted</li>
     * <li>This method does not modify the supplied {@code data} in any way</li>
     * <li>Any {@code NaN}s in {@code data} are placed at the start of the sort
     * order, regardless of sort direction</li> <ul> </p>
     * 
     * @param data to provide sort indices for
     * @param ascending if {@code true}, descending if {@code false}
     * @return an index {@code List}
     */
    public static List<Integer> sortedIndices(List<Double> data, boolean ascending) {
        checkNotNull(data);
        List<Integer> indices = Ints.asList(indices(data.size()));
        Collections.sort(indices, new IndexComparator(data, ascending));
        return indices;
    }

    /*
     * A comparator for ascending sorting of an index array based on the
     * supplied double array of data.
     */
    private static class IndexComparator implements Comparator<Integer> {
        List<Double> data;
        boolean ascending;

        IndexComparator(List<Double> data, boolean ascending) {
            this.data = data;
            this.ascending = ascending;
        }

        @Override
        public int compare(Integer i1, Integer i2) {
            double d1 = data.get(ascending ? i1 : i2);
            double d2 = data.get(ascending ? i2 : i1);
            return (d1 < d2) ? -1 : (d1 == d2) ? 0 : 1;
        }
    }

    /**
     * Returns an {@code int[]} of values ascending from {@code 0} to
     * {@code 1-length} that can be used for sorting.
     * @param length
     * @return an index array
     * @see DataUtils#sortedIndices(List, boolean)
     */
    public static int[] indices(int length) {
        int[] indices = new int[length];
        for (int i = 0; i < indices.length; i++) {
            indices[i] = i;
        }
        return indices;
    }

    /**
     * Creates an array of random {@code double} values.
     * @param length of output array
     * @return the array of random {@code double}s
     */
    public static double[] randomValues(int length) {
        Random random = new Random();
        double[] values = new double[length];
        for (int i = 0; i < length; i++) {
            values[i] = random.nextDouble();
        }
        return values;
    }

    /**
     * Returns the index of the minimum value in {@code data}.
     * @param data
     * @return the index of the minimum value
     */
    public static int minIndex(double... data) {
        int idx = -1;
        double d = Double.POSITIVE_INFINITY;
        for (int i = 0; i < data.length; i++)
            if (data[i] < d) {
                d = data[i];
                idx = i;
            }
        return idx;
    }

    /**
     * Returns the index of the maximum value in {@code data}.
     * @param data
     * @return the index of the maximum value
     */
    public static int maxIndex(double... data) {
        int idx = -1;
        double d = Double.NEGATIVE_INFINITY;
        for (int i = 0; i < data.length; i++)
            if (data[i] > d) {
                d = data[i];
                idx = i;
            }
        return idx;
    }

    /**
    * Nearest neighbor binning algorithm after Silverman, B. W. (1986),
    * <em>Density Estimation for Statistics and Data Analysis</em>, Chapman
    * &amp; Hall, New York. This method is a density estimator that uses
    * variable width binning with a fixed sample size per bin that better
    * reflects the distribution of the underlying data. It is particularly
    * useful when workgin with power-law distributed data. Bin widths are
    * computed as the difference between the last values in adjacent bins. In
    * the case of the 1st bin, the supplied origin is taken as the "last value"
    * of the previous bin. Bin positions are set from the median value in each
    * bin. Note that the supplied {@code data} is not modified; this
    * method uses a copy internally. In most cases, data will be fairly
    * continuous in X, however, for small {@code size}s it's possible to
    * have bins of identical values such that corresponding bin value is
    * Infinity. Such values are not included in the resultant data set.
    * 
    * @param data to be binned (does not need to be sorted)
    * @param origin for binning
    * @param size of each bin
    * @return an {@code XY_DataSet} of the binned distribution or
    *         {@code null} if the binned distribution is empty
    * @throws NullPointerException if the supplied {@code data} is
    *         {@code null}
    * @throws IllegalArgumentException if supplied {@code data} is empty,
    *         the bin {@code size} is &lt;1, or the {@code origin} is
    *         greater than all {@code data} values
    */
    public static DefaultXY_DataSet nearestNeighborHist(double[] data, double origin, int size) {
        checkNotNull(data, "Supplied data is null");
        checkArgument(data.length > 0, "Supplied data is empty");
        checkArgument(size > 0, "Bin size can't be less than 1");
        double[] localData = Arrays.copyOf(data, data.length);
        Arrays.sort(localData);
        int startIdx = Arrays.binarySearch(localData, origin);
        checkArgument(startIdx < localData.length, "Origin is greater than all data values");
        startIdx = (startIdx > 0) ? startIdx : -startIdx - 1;
        // for multipe identical values, binary search may not return
        // the lowest index so walk down
        while (startIdx > 0 && origin == localData[startIdx - 1])
            startIdx--;
        // trim data
        localData = Arrays.copyOfRange(localData, startIdx, localData.length);
        int binCount = (int) Math.floor(localData.length / size);
        // bail on an empty distribution
        if (binCount == 0)
            return null;
        List<Double> x = new ArrayList<Double>();
        List<Double> y = new ArrayList<Double>();
        double binLo, binHi, binDelta;
        for (int i = 0; i < binCount; i++) {
            int datIdx = i * size;
            binLo = (i == 0) ? origin : localData[datIdx - 1];
            binHi = localData[datIdx + size - 1];
            binDelta = binHi - binLo;
            // bail on intervals of identical values
            if (binDelta == 0)
                continue;
            y.add(size / (binHi - binLo));
            x.add(StatUtils.percentile(localData, datIdx, size, 50.0));
        }
        // bail on empty distribution
        return (x.isEmpty()) ? null : new DefaultXY_DataSet(x, y);
    }

    // TODO test; instances should be replaced with statistical summaries
    /**
     * Class for tracking the minimum and maximum values of a set of data.
     */
    public static class MinMaxAveTracker {
        private double min = Double.POSITIVE_INFINITY;
        private double max = Double.NEGATIVE_INFINITY;
        private double tot = 0;
        private int num = 0;

        /**
         * Add a new value to the tracker. Min/Max/Average will be updated.
         * 
         * @param val value to be added
         */
        public void addValue(double val) {
            if (val < min)
                min = val;
            if (val > max)
                max = val;
            tot += val;
            num++;
        }

        /**
         * Returns the minimum value that has been added to this tracker, or positive infinity if
         * no values have been added.
         * 
         * @return minimum value
         */
        public double getMin() {
            return min;
        }

        /**
         * Returns the maximum value that has been added to this tracker, or negative infinity if
         * no values have been added.
         * 
         * @return maximum value
         */
        public double getMax() {
            return max;
        }

        /**
         * Computes the average of all values that have been added to this tracker.
         * 
         * @return the average of all values that have been added to this tracker.
         */
        public double getAverage() {
            return tot / (double) num;
        }

        /**
         * 
         * @return total number of values added to this tracker.
         */
        public int getNum() {
            return num;
        }

        @Override
        public String toString() {
            return "min: " + min + ", max: " + max + ", avg: " + getAverage() + ", tot: " + tot;
        }
    }

    /**
     * 
     * @param unsorted
     * @return median of the array of values. if values are already sorted, use median_sorted
     */
    public static double median(double[] unsorted) {
        double[] sorted = Arrays.copyOf(unsorted, unsorted.length);
        Arrays.sort(sorted);
        return median_sorted(sorted);
    }

    /**
     * @param sorted
     * @return median of the sorted array of values
     */
    public static double median_sorted(double[] sorted) {
        if (sorted.length % 2 == 1)
            return sorted[(sorted.length + 1) / 2 - 1];
        else {
            double lower = sorted[sorted.length / 2 - 1];
            double upper = sorted[sorted.length / 2];

            return (lower + upper) * 0.5;
        }
    }

}