4
\$\begingroup\$

I am trying to do general class for basic Statistical Measures mode, arithmetic mean,geometric mean, median , variance ,and stander division functions.

I am looking for some general feedback on how I can improve the structure and efficiency of my code. and if there is better way to do this.

StatisticMeasures.java

package analysis.statistic;

import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;

/**
 * The class StatisticMeasures contains methods for performing basic Statistical
 * Measures such as mode, arithmetic mean,geometric mean, median , variance ,and
 * stander division functions.
 * 
 * @author Eslam Ali
 *
 */
public class StatisticMeasures {

    /**
     * Calculates the average of the squared differences from the Mean.
     */
    public static double variance(int... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        double mean = arithmeticMean(numbers);
        return (double) IntStream.of(numbers).map(i -> (int) Math.pow((i - mean), 2)).sum() / numbers.length;
    }

    /**
     * Work like {@link #variance(int...)}.
     */
    public static double variance(double... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        double mean = arithmeticMean(numbers);
        return (double) DoubleStream.of(numbers).map(i -> (int) Math.pow((i - mean), 2)).sum() / numbers.length;
    }

    /**
     * Work like {@link #variance(int...)}.
     */
    public static double variance(long... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        double mean = arithmeticMean(numbers);
        return (double) LongStream.of(numbers).map(i -> (int) Math.pow((i - mean), 2)).sum() / numbers.length;
    }

    /**
     * Calculates square root of the Variance of list of numbers.
     */
    public static double standardDeviation(int... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        return StrictMath.sqrt(Variance.variance(numbers));
    }

    /**
     * Work like {@link #standardDeviation(int...)}.
     */
    public static double standardDeviation(long... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        return StrictMath.sqrt(Variance.variance(numbers));
    }

    /**
     * Work like {@link #standardDeviation(int...)}.
     */
    public static double standardDeviation(double... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        return StrictMath.sqrt(Variance.variance(numbers));
    }

    /**
     * Return middle of list of numbers after sort it
     * 
     * @return middle of a sorted list of numbers.
     */
    public static double median(int... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        int length = numbers.length;
        if (length == 0) {
            return Double.NaN;
        }
        Arrays.sort(numbers);

        if (length % 2 == 0) {
            return (double) (numbers[length / 2] + numbers[(length / 2) + 1]) / 2;
        } else {
            return numbers[(length + 1) / 2];
        }

    }

    /**
     * Work like {@link #median(int...)}
     */
    public static double median(long... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        int length = numbers.length;
        if (length == 0) {
            return Double.NaN;
        }
        Arrays.sort(numbers);

        if (length % 2 == 0) {
            return (double) (numbers[length / 2] + numbers[(length / 2) + 1]) / 2;
        } else {
            return numbers[(length + 1) / 2];
        }

    }

    /**
     * Work like {@link #median(int...)}
     */
    public static double median(double... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        int length = numbers.length;
        if (length == 0) {
            return Double.NaN;
        }
        Arrays.sort(numbers);

        if (length % 2 == 0) {
            return (double) (numbers[length / 2] + numbers[(length / 2) + 1]) / 2;
        } else {
            return numbers[(length + 1) / 2];
        }

    }

    /**
     * Work like {@link #median(int...)}
     */
    public static double median(float... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        int length = numbers.length;
        if (length == 0) {
            return Double.NaN;
        }
        Arrays.sort(numbers);

        if (length % 2 == 0) {
            return (double) (numbers[length / 2] + numbers[(length / 2) + 1]) / 2;
        } else {
            return numbers[(length + 1) / 2];
        }

    }

    // after consider feedbacks
    // https://codereview.stackexchange.com/questions/172184/general-java-class-to-find-mode

    /**
     * Return objects which appears most often.
     * 
     * @return Map<object,countAppears> most appears objects.
     */
    @SuppressWarnings("unchecked")
    public static <T extends Comparable<? super T>> ModePair<T> mode(T... objects) {
        Objects.requireNonNull(objects, "objects must not be null");
        if (objects.length == 0) {
            return new ModePair<>(new HashSet<>(), 0);
        }
        Arrays.sort(objects);
        Mode<T> mode = new Mode<T>(objects[0]);
        for (T t : objects) {
            mode.checkMaxAppears(t);
        }

        return mode.getMode();

    }

    /**
     * Work like {@link #mode(Object...)}.
     */
    @SuppressWarnings("unchecked")
    public static <T extends Comparable<? super T>> ModePair<T> mode(Comparator<? super T> c, T... objects) {
        Objects.requireNonNull(objects, "objects must not be null");
        if (objects.length == 0) {
            return new ModePair<>(new HashSet<>(), 0);
        }
        Arrays.sort(objects, c);
        Mode<T> mode = new Mode<T>(objects[0]);
        for (T t : objects) {
            mode.checkMaxAppears(t);

        }

        return mode.getMode();

    }

    /**
     * Work like {@link #mode(Object...)}.
     */
    public static ModePair<Integer> mode(int... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        if (numbers.length == 0) {
            return new ModePair<>(new HashSet<>(), 0);
        }
        Arrays.sort(numbers);
        Mode<Integer> mode = new Mode<Integer>(numbers[0]);
        for (int t : numbers) {
            mode.checkMaxAppears(t);
        }

        return mode.getMode();
    }

    /**
     * Work like {@link #mode(Object...)}.
     */
    public static ModePair<Long> mode(long... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        if (numbers.length == 0) {
            return new ModePair<>(new HashSet<>(), 0);
        }
        Arrays.sort(numbers);
        Mode<Long> mode = new Mode<>(numbers[0]);
        for (long t : numbers) {
            mode.checkMaxAppears(t);

        }

        return mode.getMode();
    }

    /**
     * Work like {@link #mode(Object...)}.
     */
    public static ModePair<Double> mode(double... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        if (numbers.length == 0) {
            return new ModePair<>(new HashSet<>(), 0);
        }
        Arrays.sort(numbers);
        Mode<Double> mode = new Mode<Double>(numbers[0]);
        for (double t : numbers) {
            mode.checkMaxAppears(t);
        }
        return mode.getMode();
    }

    /**
     * Work like {@link #mode(Object...)}.
     */
    public static ModePair<Float> mode(float... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");

        if (numbers.length == 0) {
            return new ModePair<>(new HashSet<>(), 0);
        }

        Arrays.sort(numbers);
        Mode<Float> mode = new Mode<Float>(numbers[0]);
        for (float t : numbers) {
            mode.checkMaxAppears(t);
        }
        return mode.getMode();
    }

    /**
     * Work like {@link #mode(Object...)}.
     */
    public static ModePair<String> mode(String... strings) {
        Objects.requireNonNull(strings, "strings must not be null");
        if (strings.length == 0) {
            return new ModePair<>(new HashSet<>(), 0);
        }
        Arrays.sort(strings);
        Mode<String> mode = new Mode<>(strings[0]);
        for (String t : strings) {
            mode.checkMaxAppears(t);
        }
        return mode.getMode();
    }

    private static class Mode<T> {
        private int nTimesLastObjectAppears = 0;
        private int maxTimeObjectAppears = 0;
        private T prevObject;
        Set<T> mostAppearsObjects;

        public Mode(T firstObjectInArray) {
            prevObject = firstObjectInArray;
            mostAppearsObjects = new HashSet<>();
        }

        void checkMaxAppears(T currentObject) {
            if (currentObject.equals(prevObject)) {
                nTimesLastObjectAppears += 1;
            } else {
                addObjectToMap();
                prevObject = currentObject;
                nTimesLastObjectAppears = 1;
            }
        }

        void addObjectToMap() {
            if (nTimesLastObjectAppears > maxTimeObjectAppears) {
                mostAppearsObjects.clear();
                mostAppearsObjects.add(prevObject);
                maxTimeObjectAppears = nTimesLastObjectAppears;
            } else if (nTimesLastObjectAppears == maxTimeObjectAppears) {
                mostAppearsObjects.add(prevObject);
            }
        }

        ModePair<T> getMode() {
            // to check appears of last object of loop and add it to map
            addObjectToMap();
            return new ModePair<>(mostAppearsObjects, maxTimeObjectAppears);
        }
    }

    // after consider feedbacks
    // https://codereview.stackexchange.com/questions/172138/calculate-geometric-and-arithmetic-mean

    /**
     * Calculates geometric mean of the given numbers .
     * 
     * <ul>
     * <li>(-1)^m * 1/n-rt(product(numbers)) = (-1)^m exp(1/n
     * sum(ln(numbers[i])))
     * <li>n : is length of numbers</li>
     * <li>m : is number of negative values</li>
     * </ul>
     * 
     * @return geometric mean
     *         <ul>
     *         <li>NAN : if numbers array is empty</li>
     *         <li>0 :if numbers array contain 0 value</li>
     *         <li>negative value : if numbers of negative values is odd
     *         <li>positive value : if numbers of positive values is even or it
     *         just positive values
     *         </ul>
     * 
     * @throws IllegalArgumentException
     *             if numbers array are null
     */
    public static double geometricMean(int... numbers) {
        if (numbers == null) {
            throw new IllegalArgumentException("numbers must be not null");
        }

        if (numbers.length == 0) {
            return Double.NaN;
        }

        Mean stats = new Mean();

        for (int i : numbers) {
            if (i == 0) {
                return 0.0;
            }
            stats.add(i);

        }

        return stats.getGeometricMean();

    }

    /**
     * Works just like {@link Mean#geometricMean(int...)} except the array
     * contains long numbers.
     */
    public static double geometricMean(long... numbers) {

        if (numbers == null) {
            throw new IllegalArgumentException("numbers must be not null");
        }

        if (numbers.length == 0) {
            return Double.NaN;
        }

        Mean stats = new Mean();
        for (long i : numbers) {
            if (i == 0) {
                return 0.0;
            }
            stats.add(i);

        }

        return stats.getGeometricMean();

    }

    /**
     * Works just like {@link Mean#Mean#geometricMean(int...)} except the array
     * contains double numbers.
     */
    public static double geometricMean(double... numbers) {

        if (numbers == null) {
            throw new IllegalArgumentException("numbers must be not null");
        }

        if (numbers.length == 0) {
            return Double.NaN;
        }

        Mean mean = new Mean();

        for (double i : numbers) {
            if (i == 0) {
                return 0.0;
            } else if (i == Double.NaN) {
                return Double.NaN;
            }
            mean.add(i);
        }

        return mean.getGeometricMean();

    }

    /**
     * Works just like {@link Mean#geometricMean(int...)} except the array
     * contains float numbers
     */
    public static double geometricMean(float... numbers) {

        Objects.requireNonNull(numbers, "numbers must not be null");
        if (numbers.length == 0) {
            return Double.NaN;
        }
        Mean mean = new Mean();

        for (float i : numbers) {
            if (i == 0) {
                return 0;
            } else if (i == Float.NaN) {
                return Double.NaN;
            }
            mean.add(i);
        }

        return mean.getGeometricMean();

    }

    private static class Mean {
        private int n;
        private double logsum;
        private double sign = 1.0;

        void add(double num) {
            n++;
            if (num < 0) {
                logsum += Math.log(-num);
                sign = -sign;
            } else {
                logsum += StrictMath.log(num);
            }
        }

        double getGeometricMean() {
            return sign * StrictMath.exp(logsum / n);
        }
    }

    /**
     * The mean is the average of the numbers.
     * <ul>
     * <li>sum(numbers[i])/n</li>
     * <li>n : length of array numbers.</li> integers
     * </ul>
     * 
     * @return average of the numbers
     *         <ul>
     *         <li>NAN : if array numbers is empty
     *         <li>average : if contains numbers
     *         </ul>
     * @throws IllegalArgumentException
     *             if numbers array is null
     */
    public static double arithmeticMean(int... numbers) {
        Objects.requireNonNull(numbers, "numbers must not be null");
        long tmp = 0;
        for (int i : numbers) {
            tmp += i;

        }
        return (double) tmp / numbers.length;

    }

    /**
     * Works just like {@link Mean#arithmeticMean(int...)} except the array
     * contains double numbers.
     */
    public static double arithmeticMean(double... numbers) {
        return DoubleStream.of(numbers).sum() / numbers.length;
    }

    /**
     * Works just like {@link Mean#arithmeticMean(int...)} except the array
     * contains long numbers .
     */
    public static double arithmeticMean(long... numbers) {
        return (double) LongStream.of(numbers).sum() / numbers.length;
    }

}

ModePair.java

package analysis.statistic;

import java.util.Collections;
import java.util.Set;

public class ModePair<T> {
    private Set<T> values;
    private int nFrequence;
    public ModePair(Set<T> values,int nFrequence) {
        this.values = Collections.unmodifiableSet(values);
        this.nFrequence = nFrequence;
    }
    public int getnFrequence() {
        return nFrequence;
    }

    public Set<T> getValues() {
        return values;
    }

}
\$\endgroup\$
1
\$\begingroup\$

First thing I'd add is some input validation. You already check for a null input but what about an empty sequence? You get ArithmeticException which may, or may not, be appropriate (but for sure not descriptive enough for production code).

Average (and then many other statistical indices) has no meaning for an empty set (unless you are calculating the Fréchet mean) then I'd pick one of these:

  • Validate input to throw an appropriate and descriptive exception (what it's done, for example, in .NET for LINQ implementation).
  • Return NaN, which seems an appropriate value for a non existing value (what it's done, for example, in languages without support for optional values and exceptions).
  • Return Optional<T> with a null value when sequence contains no elements. It's the design of many Java aggregation functions (for example Interface<T>.sum()) or OptionalDouble (like IntStream.average()).
  • Throw NullPointerException like, for example, findFirst() does. Not exactly what user may expect for a numerical computation, I think.

Note that you already handle this case in median(), whatever is the way you decide to go (I'd stick to Java modern conventions, BTW) you should be consistent.


StatisticMeasures is (IMO) a catch-all name. If you will add more statistical functions you may want to group them better. So far you have only DescriptiveStatistic functions then you can start with that name.


It's common (but opinionated) to throw NullPointerException for an invalid null argument and Java itself isn't consistent about this but you're currently doing both ways: Objects.requireNonNull() throws NPE and inside, for example, geometricMean() you throw IAE. Stick to one of them and clearly document what you throw!


I have some doubts about the way you calculate the geometric mean. You're using logarithms and log(0) is undefined but returning 0 as result isn't correct! There are other commonly used workarounds for that:

  • Exclude 0 from calculation.
  • Convert 0 to 1.
  • If there is any 0 then add 1 to each value (including 0) and then subtract 1 from the result.

If you really don't want to handle this case then return NaN or throw an exception.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.