1
\$\begingroup\$

Looking for any feedback on improvements that could be made to this class. I am attempting to represent a 2-dimensional data set(one variable input, one variable output). I have included all analytical computations I could think of. I am open to feedback for new features as well as review changes to functionality.

package statTool;

import java.util.ArrayList;
import javafx.util.Pair;

/**
 * This class is used to model a data sampled from a standard distribution, and computes several values used
 * to analyze the behavior of the population to which the sample belongs.
 * The values computed and retained for both input and output are:
 *      Mean
 *      Min/Max 
 *      Sum of Squared Error
 *      Mean Squared Error(Variance)
 *      Standard Deviation (Standard Error)
 *      Sum
 *      Square Sum
 *Singular Variables are:
 *      R correlation
 *      Covariance
 *      Linear fit equation
 *      input*output Product Sum  
 * @author wood
 */
public class XYSample {
    private float size;
    private float xMean, xMin, xMax, xVariance, xError, xSumSquaredError;
    private float yMean, yMin, yMax, yVariance, yError, ySumSquaredError;
    private float xSum, ySum, xySum, x2Sum, y2Sum;
    private float R, covariance;

    LinearEquation fitFunction;


    //Using ArrayList for the AddAll function
    private ArrayList<Float> X;
    private ArrayList<Float> Y;
    //--------------------------------------------------------------------------------------------------------------
    // Constructors
    // --------------------------------------------------------------------------------------------------------------
    public XYSample() {
        initSample();
    }

    public XYSample(ArrayList<Pair<Float, Float>> data){
        initSample(); 
        addValues(data);
    }

    public XYSample(ArrayList<Float> xData, ArrayList<Float> yData) {
        initSample();
        addValues(xData, yData);
    }

    private void initSample(){
        size = 0;

        //Initialize List
        X = new ArrayList<Float>();
        Y = new ArrayList<Float>();

        //Initialize comparator values
        xMin = Float.MAX_VALUE;
        yMin = Float.MAX_VALUE;
        xMax = Float.MIN_VALUE;
        yMax = Float.MIN_VALUE;
    }

    //--------------------------------------------------------------------------------------------------------------
    //      Populate Sample
    //--------------------------------------------------------------------------------------------------------------

    //As the above suggests, the below methods serve to extract values from ArrayLists and add them to the 
    //appropriate input or output list

    /**
     * Splits pairData into two lists of input and output then calls addValues
     * @param toAdd
     */
    public void addValues(ArrayList<Pair<Float,Float>> toAdd) {
        ArrayList<Float> input = new ArrayList<Float>();
        ArrayList<Float> output = new ArrayList<Float>();

        for(Pair<Float,Float> pair : toAdd){
            input.add(pair.getKey());
            output.add(pair.getValue());
        }
    }

    /**
     * This method allows the user to add additional values to the existing data set
     * Checks for new max or min now, to avoid iterating through the entire input/output set needlessly,
     * then calls setValues() to recalculate sample analysis
     * @param toAdd
     */
    public void addValues(ArrayList<Float> input, ArrayList<Float> output) {
        X.addAll(input);
        Y.addAll(output);

        //Check input minimum and maximum
        float temp;
        for(int i = 0; i < input.size(); i++){
            temp = input.get(i);
            if(temp > xMax){
                xMax = temp;
            }
            if(temp < xMin){
                xMin = temp;
            }
        }

        //Check output minimum and maximum
        for(int i = 0; i < output.size(); i++){
            temp = output.get(i);
            if(temp > yMax){
                yMax = temp;
            }
            if(temp < yMin){
                yMin = temp;
            }
        }

        setValues();
    }
    //--------------------------------------------------------------------------------------------------------------
    //      Basic Analysis
    //--------------------------------------------------------------------------------------------------------------

    //The method below is called every time the sample is changed. It initializes each basic analytical value

    private void setValues() {
        size = (float)X.size();
        xSum = sum(X);
        ySum = sum(Y);
        xMean = mean(xSum);
        yMean = mean(ySum);
        xSumSquaredError = squaredError(X, xMean);
        ySumSquaredError = squaredError(Y, yMean);
        xVariance = variance(xSumSquaredError);
        yVariance = variance(ySumSquaredError);
        xError = standardError(xSumSquaredError);
        yError = standardError(ySumSquaredError);
        x2Sum = squareSum(X);
        y2Sum = squareSum(Y);
        xySum = productSum(X,Y);
        R = correlation();
        covariance = covariance();
        fitFunction = linearFit();
    }

    /**
     * s the Sample Mean by creating a running summation of the values and then dividing by the
     * number of values in the set
     * @return double
     */
    private Float mean(float sum) {
        return sum / size;
    }

    /**
     * s the Sum of the Squared Error for the sample, which is used to  the variance and 
     * standard error
     * @return double
     */
    private float squaredError(ArrayList<Float> data, float mean){
        float temp;
        float tempSum = 0;
        for (float value: data) {
            temp = (float) Math.pow(value - mean, 2);
            tempSum += temp;
        }
        return tempSum;
    }

    /**
     * The sample variance carries a bias of n-1/n, where n is the size of the sample. Multiplying this values 
     * by n/n-1 removes this bias as an estimate of the population variance. This results in the variance 
     * being calculated with n-1 as opposed to n
     * @return double
     */
    private float variance(float sumsquaredError) {
        return sumsquaredError / (size-1);
    }

    /**
     * As a population estimate, the samples standard error carries a bias of (sqrt(n-1.5)/sqrt(n)). Removing
     * this bias, as above with variance, results in calculating with sqrt(n-1.5) as the denominator
     * @return
     */
    private float standardError(float sumSquaredError){
        return (float) Math.sqrt(sumSquaredError / (size-1.5));
    }
    //--------------------------------------------------------------------------------------------------------------
    //      Summations
    //--------------------------------------------------------------------------------------------------------------

    //The methods below return summations of the given data

    private float sum(ArrayList<Float> data){
        float tempSum = 0;
        for(float item : data){
            tempSum += item;
        }
        return tempSum;
    }

    private float productSum(ArrayList<Float> data1, ArrayList<Float> data2)
    {
        float tempSum = 0;
        for(int i = 0; i < data1.size(); i++){
            tempSum += (data1.get(i)* data2.get(i));
        }
        return tempSum;
    }

    private float squareSum(ArrayList<Float> data){
        float tempSum = 0;
        for(float item: data){
            tempSum += Math.pow(item, 2);
        }
        return tempSum;
    }
    //--------------------------------------------------------------------------------------------------------------
    //      Regression Analysis
    //--------------------------------------------------------------------------------------------------------------        

    //The methods below perform regression on the samples input and output to  a linear equation
    //of form Slope*(input) + Intercept = (output). R^2 correlation is returned as a decimal between 0 and 1

    private float correlation(){
        float numerator = (X.size() * xySum) - (xSum * ySum);
        float denominatorLeft = (X.size() * x2Sum) - ((float)Math.pow(xSum, 2));
        float denominatorRight = (Y.size() * y2Sum) - ((float)Math.pow(ySum, 2));

        return numerator/((float)Math.sqrt(denominatorLeft*denominatorRight));  
    }

    private float covariance(){
        float runSum = 0;
        for(int i = 0; i < X.size(); i++){
            runSum += (X.get(i) - xMean) * (Y.get(i) - yMean);
        }
        return runSum/(X.size() -1);
    }

    private LinearEquation linearFit(){         
        float slope = slope(xySum, xSum, ySum, x2Sum);
        float intercept = intercept(xySum, xSum, ySum, x2Sum);


        LinearEquation toReturn = new LinearEquation(slope, intercept);
        return toReturn;
    }

    private float slope(float xySum, float xSum, float ySum, float x2Sum) {
        float numerator = (X.size()*xySum) - (xSum*ySum);
        float denominator = (X.size()*x2Sum) - (float)Math.pow(xSum, 2);
        return numerator/denominator;
    }

    private float intercept(float xySum, float xSum, float ySum, float x2Sum) {
        float numerator = (ySum*x2Sum) - (xSum*xySum);
        float denominator = (X.size()*x2Sum) - (float)Math.pow(xSum, 2);
        return numerator/denominator;
    }

    //--------------------------------------------------------------------------------------------------------------
    //      Getters
    //--------------------------------------------------------------------------------------------------------------
    public float getSize(){return size;}
    public float getXMean(){return xMean;}
    public float getYMean(){return yMean;}
    public float getXMin(){return xMin;}
    public float getYMin(){return yMin;}
    public float getXMax(){return xMax;}
    public float getYMax(){return yMax;}
    public float getXVariance(){return xVariance;}
    public float getYVariance(){return yVariance;}
    public float getXError(){return xError;}
    public float getYError(){return yError;}
    public float getXSumsquaredError(){return xSumSquaredError;}
    public float getYSumsquaredError(){return ySumSquaredError;}
    public float getXSum(){return xSum;}
    public float getYSum(){return ySum;}
    public float getXSquareSum(){return x2Sum;}
    public float getYSquareSum(){return y2Sum;}
    public float getProductSum(){return xySum;}     
    public float getR(){return R;}
    public float getRSquare(){return (float)Math.pow(R,2);}
    public float getCovariance(){return covariance;}
    public LinearEquation getLinearFit(){return fitFunction;}
}
\$\endgroup\$
  • \$\begingroup\$ Write a JUnit test for all your methods. I bet addValues(ArrayList<Pair<Float,Float>> toAdd) will fail. \$\endgroup\$ – Ralf Kleberhoff Dec 15 '17 at 11:54
  • \$\begingroup\$ And test the max values with only negative numbers, as Float.MIN_VALUE is a very small positive number and not near minus infinity. \$\endgroup\$ – Ralf Kleberhoff Dec 15 '17 at 12:23
1
\$\begingroup\$

I haven't analyzed all your code. But here are a few remarks:

You call setValues() after all additions to the samples, and that method computes everything from scratch. You could instead:

  • maintain all statistical accumulators (xSum, xySum etc.) and update them incrementally with only the new values, or
  • switch to lazy evaluation: introduce a valid flag that you clear whenever you modify the samples, and implement a validate() method that recomputes the results if the flag is cleared. You'd then call validate() in all your getters before returning the value.

You're inconsistent in some aspects:

  • You compute the min and max values while adding samples, but compute the other values in setValues().
  • The mean() method returns a Float object, while the others return simple float primitives.

You have public methods that require your user to pass in an ArrayList. You should change that to List, thus allowing for arbitrary List implementations.

I don't understand why you made size a float insted of an int. It can never be fractional, and if it's for performance reasons, it's "premature optimization".

And may I suggest to rename the class to XYSamples, as XYSample to me sounds like one x/y pair only, and to use a package name beginning with some reverse domain name you are associated with, like com.glass.wood.statistics or similar, so you don't risk collision with some other library.

\$\endgroup\$
  • \$\begingroup\$ Size is a float as it is used in computations with other floats(see variance for example) and int with casting was giving me issues in testing. The min and max a located in add values to prevent reiteration. Say the sample is already populated with some values for which min and max have been found. Checking only the new values being added prevents reiterating over the previous values. This means we same some time by only checking if the new values contain a smaller min or a larger max. \$\endgroup\$ – DapperDan Dec 15 '17 at 17:00
  • \$\begingroup\$ To make sure I am understanding your "lazy evaluation" suggestion: Each getter would thus be something like getter(){if(!validate()){computeValue();} return someValue;}. OR, do you mean that validate would do nothing if valid were true,and recompute all if false? \$\endgroup\$ – DapperDan Dec 15 '17 at 17:05
  • 1
    \$\begingroup\$ I'd prefer getter() { validate(); return value; } and have validate() check for the flag and compute if necessary. \$\endgroup\$ – Ralf Kleberhoff Dec 15 '17 at 18:48
  • 1
    \$\begingroup\$ The same thing you do with min and max (update only from new data), you can also do with the sums like xSum etc. \$\endgroup\$ – Ralf Kleberhoff Dec 15 '17 at 18:50

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.