1
\$\begingroup\$

I've been playing with a statistical Sample object for an input/output model during the course of my machine learning class. I wanted to expand the functionality to add quadratic fit by least squares method, but I want to make sure the functionality is thus far is up to par before improving it further. Mainly just looking for feedback, improvements, comments, anything!

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
 *      Linear fit equation
 *      input*output Product Sum  
 * @author B19635
 */
public class XYSample {
        float size;
        float xMean, xMin, xMax, xVariance, xError, xSumSquaredError;
        float yMean, yMin, yMax, yVariance, yError, ySumSquaredError;
        float xSum, ySum, xySum, x2Sum, y2Sum;
        float R;

        LinearEquation fitFunction;

        ArrayList<Float> X, 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(int i = 0; i < toAdd.size(); i++){
                input.add(toAdd.get(i).getKey());
            }

            for(int i = 0; i < toAdd.size(); i++){
                output.add(toAdd.get(i).getValue());
            }
            addValues(input, output);
        }

        /**
         * 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();
            xMean = computeMean(X);
            yMean = computeMean(Y);
            xSumSquaredError = computesquaredError(X, xMean);
            ySumSquaredError = computesquaredError(Y, yMean);
            xVariance = computeVariance(xSumSquaredError);
            yVariance = computeVariance(ySumSquaredError);
            xError = computeStandardError(xSumSquaredError);
            yError = computeStandardError(ySumSquaredError);
            xSum = computeSum(X);
            ySum = computeSum(Y);
            x2Sum = computeSquareSum(X);
            y2Sum = computeSquareSum(Y);
            xySum = computeProductSum(X,Y);
            R = computeCorrelation();
            fitFunction = computeLinearFit();
        }

        /**
         * Computes 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 computeMean(ArrayList<Float> data) {
            float runSum = 0;
            for (float value: data) {
                runSum += value;
            }
            return runSum / size;
        }

        /**
         * Computes the Sum of the Squared Error for the sample, which is used to compute the variance and 
         * standard error
         * @return double
         */
        private float computesquaredError(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 computeVariance(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 computeStandardError(float sumSquaredError){
            return (float) Math.sqrt(sumSquaredError / (size-1.5));
        }
        //--------------------------------------------------------------------------------------------------------------
        //      Summations
        //--------------------------------------------------------------------------------------------------------------

        //The methods below return summations of the given data

        private float computeSum(ArrayList<Float> data){
            float tempSum = 0;
            for(int i = 0; i < data.size(); i++){
                tempSum += data.get(i);
            }
            return tempSum;
        }

        private float computeProductSum(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 computeSquareSum(ArrayList<Float> data){
            float tempSum = 0;
            for(int i = 0; i < data.size(); i++){
                tempSum += Math.pow(data.get(i), 2);
            }
            return tempSum;
        }
        //--------------------------------------------------------------------------------------------------------------
        //      Regression Analysis
        //--------------------------------------------------------------------------------------------------------------        

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

        private float computeCorrelation(){
            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 LinearEquation computeLinearFit(){          
            float slope = computeSlope(xySum, xSum, ySum, x2Sum);
            float intercept = computeIntercept(xySum, xSum, ySum, x2Sum);

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

        private float computeSlope(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 computeIntercept(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 LinearEquation getLinearFit(){return fitFunction;}
    }

public class LinearEquation {
    float slope, intercept;

    public LinearEquation(float slope, float intercept) {
        this.slope = slope;
        this.intercept = intercept;
    }

    public float f(float input){
        return (input * slope) + intercept;
    }
    public float getSlope(){
        return slope;
    }

    public float getIntercept(){
        return intercept;
    }
}
\$\endgroup\$
2
\$\begingroup\$
        ArrayList<Float> X, Y;

Consider

        private List<Float> X;
        private List<Float> Y;

Because this is split on two lines, it is easier to see that we are declaring two variables.

Now we can easily see that the visibility of these variables is private. Previously they were the default, package-private. So any class in the same package could access them directly. While there are reasons to use package-private, I would normally expect to see those documented in comments. E.g. "Setting these to package-private so class Foo can access them directly, because blah blah." Of course, replace Foo and blah blah with an actual class and reason.

By convention, all class fields are set to private unless we have some specific reason for making them something else. It doesn't have to be a great reason, but there should be one. That way you won't rely on another visibility unnecessarily.

As a general rule, declare variables as the interface rather than the implementation. This allows you to switch implementations easily. Also, it helps avoid using methods specific to the implementation when the interface is sufficient. In the rare cases where it is insufficient, I would expect that to be commented. "Using ArrayList because I need the foo method, which is not available in the List interface." Of course, replace ArrayList, List, and foo with the correct names when that happens.

        public XYSample(ArrayList<Pair<Float, Float>> data){

Similarly, this could be

        public XYSample(List<Pair<Float, Float>> data) {

No need to restrict to ArrayList that I saw.

            for(int i = 0; i < toAdd.size(); i++){
                input.add(toAdd.get(i).getKey());
            }

You can save managing the i variable with

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

The code ends up slightly shorter too, but the main advantage is that you don't try to micromanage the iteration.

            xMean = computeMean(X);
            yMean = computeMean(Y);
            xSumSquaredError = computesquaredError(X, xMean);
            ySumSquaredError = computesquaredError(Y, yMean);
            xVariance = computeVariance(xSumSquaredError);
            yVariance = computeVariance(ySumSquaredError);
            xError = computeStandardError(xSumSquaredError);
            yError = computeStandardError(ySumSquaredError);
            xSum = computeSum(X);
            ySum = computeSum(Y);

You compute the means first, but the means use the sum. Why not change the order?

            xSum = computeSum(X);
            ySum = computeSum(Y);
            xMean = computeMean(xSum);
            yMean = computeMean(ySum);
            xSumSquaredError = computesquaredError(X, xMean);
            ySumSquaredError = computesquaredError(Y, yMean);
            xVariance = computeVariance(xSumSquaredError);
            yVariance = computeVariance(ySumSquaredError);
            xError = computeStandardError(xSumSquaredError);
            yError = computeStandardError(ySumSquaredError);

Then

        private Float computeMean(ArrayList<Float> data) {
            float runSum = 0;
            for (float value: data) {
                runSum += value;
            }
            return runSum / size;
        }

could just be

        private Float computeMean(float sum) {
            return sum / size;
        }

No need to calculate the sums twice.

\$\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.