2
\$\begingroup\$

Functionality

This class has a list of statistical functions such as mean, variance, standard deviation, skewness, and it works okay. These functions are applied to arrays of charts data.

It is an extension of a mother class that roughly estimates very close future prices of a list of equities using an API delayed 60-seconds chart data.

(ConstEQ is only a list of const variables and all related codes can be viewed in this link.)


Reviewing

Would you be so kind and possibly review this class, maybe for performance, efficiency, math, best coding practices or a list of changes with a list of to-be-changed to?


Class ST

// Config class for path and other constants
require_once __DIR__ . "/ConstEQ.php";

/**
 * This is an extended class with basic statistical method
 * Stock
 */
class ST extends EQ implements ConstEQ
{

    /**
     *
     * @return a number equal to mean of values of an array
     */
    public static function getMean($array)
    {
        if (count($array) == 0) {
            return ConstEQ::NEAR_ZERO_NUMBER;
        } else {
            return array_sum($array) / count($array);
        }
    }

    /**
     *
     * @return a number normalized in between 0 to 1
     */
    public static function getNormalize($value, $min, $max)
    {
        if ($max - $min != 0) {
            $normalized = 2 * (($value - $min) / ($max - $min)) - 1;
        } else {
            $normalized = 2 * (($value - $min)) - 1;
        }
        return $normalized;
    }

    /**
     *
     * @return a number normalized in between 0.0 to 1 from any input -inf to inf
     */
    public static function getSigmoid($t)
    {
        return 1 / (1 + pow(M_EULER, -$t));
    }

    /**
     *
     * @return a number equal to square of value mean
     */
    public static function getMeanSquare($x, $mean)
    {
        return pow($x - $mean, 2);
    }

    /**
     *
     * @return a number equal to standard deviation of values of an array
     */
    public static function getStandardDeviation($array)
    {
        if (count($array) < 2) {
            return ConstEQ::NEAR_ZERO_NUMBER;
        } else {
            return sqrt(array_sum(array_map("ST::getMeanSquare", $array, array_fill(0, count($array), (array_sum($array) / count($array))))) / (count($array) - 1));
        }
    }

    /**
     *
     * @return a number equal to covariance of values of two arrays
     */
    public static function getCovariance($valuesA, $valuesB)
    {
        // sizing both arrays the same, if different sizes
        $no_keys = min(count($valuesA), count($valuesB));
        $valuesA = array_slice($valuesA, 0, $no_keys);
        $valuesB = array_slice($valuesB, 0, $no_keys);

        // if size of arrays is too small
        if ($no_keys < 2) {return ConstEQ::NEAR_ZERO_NUMBER;}

        // Use library function if available
        if (function_exists('stats_covariance')) {return stats_covariance($valuesA, $valuesB);}

        $meanA = array_sum($valuesA) / $no_keys;
        $meanB = array_sum($valuesB) / $no_keys;
        $add = 0.0;

        for ($pos = 0; $pos < $no_keys; $pos++) {
            $valueA = $valuesA[$pos];
            if (!is_numeric($valueA)) {
                trigger_error('Not numerical value in array A at position ' . $pos . ', value=' . $valueA, E_USER_WARNING);
                return false;
            }

            $valueB = $valuesB[$pos];
            if (!is_numeric($valueB)) {
                trigger_error('Not numerical value in array B at position ' . $pos . ', value=' . $valueB, E_USER_WARNING);
                return false;
            }

            $difA = $valueA - $meanA;
            $difB = $valueB - $meanB;
            $add += ($difA * $difB);
        }

        return $add / $no_keys;
    }

    /**
     *
     * @return a number equal to skewness of array values
     */
    public static function getSkewness($values)
    {
        $numValues = count($values);
        if ($numValues == 0) {return 0.0;}

        // Uses function from php_stats library if available
        if (function_exists('stats_skew')) {return stats_skew($values);}

        $mean = array_sum($values) / floatval($numValues);

        $add2 = 0.0;
        $add3 = 0.0;

        foreach ($values as $value) {
            if (!is_numeric($value)) {return false;}

            $dif = $value - $mean;
            $add2 += ($dif * $dif);
            $add3 += ($dif * $dif * $dif);

        }

        $variance = $add2 / floatval($numValues);

        if ($variance == 0) {return ConstEQ::NEAR_ZERO_NUMBER;} else {return ($add3 / floatval($numValues)) / pow($variance, 3 / 2.0);}
    }

    /**
     *
     * @return a number equal to kurtosis of array values
     */
    public static function getKurtosis($values)
    {
        $numValues = count($values);
        if ($numValues == 0) {return 0.0;}

        // Uses function from php_stats library if available
        if (function_exists('stats_kurtosis')) {return stats_kurtosis($values);}

        $mean = array_sum($values) / floatval($numValues);
        $add2 = 0.0;
        $add4 = 0.0;

        foreach ($values as $value) {
            if (!is_numeric($value)) {return false;}
            $dif = $value - $mean;
            $dif2 = $dif * $dif;
            $add2 += $dif2;
            $add4 += ($dif2 * $dif2);
        }

        $variance = $add2 / floatval($numValues);
        if ($variance == 0) {return ConstEQ::NEAR_ZERO_NUMBER;} else {return ($add4 * $numValues) / ($add2 * $add2) - 3.0;}
    }

    /**
     *
     * @return a number equal to correlation of two arrays
     */
    public static function getCorrelation($arr1, $arr2)
    {
        $correlation = 0;

        $k = ST::sumProductMeanDeviation($arr1, $arr2);
        $ssmd1 = ST::sumSquareMeanDeviation($arr1);
        $ssmd2 = ST::sumSquareMeanDeviation($arr2);

        $product = $ssmd1 * $ssmd2;

        $res = sqrt($product);
        if ($res == 0) {return ConstEQ::NEAR_ZERO_NUMBER;}
        $correlation = $k / $res;

        if ($correlation == 0) {return ConstEQ::NEAR_ZERO_NUMBER;} else {return $correlation;}
    }

    /**
     *
     * @return a number equal to sum of product mean deviation of each array values
     */
    public static function sumProductMeanDeviation($arr1, $arr2)
    {
        $sum = 0;
        $num = count($arr1);

        for ($i = 0; $i < $num; $i++) {$sum = $sum + ST::productMeanDeviation($arr1, $arr2, $i);}
        return $sum;
    }

    /**
     *
     * @return a number equal to product mean deviation of each array values
     */
    public static function productMeanDeviation($arr1, $arr2, $item)
    {
        return (ST::meanDeviation($arr1, $item) * ST::meanDeviation($arr2, $item));
    }

    /**
     *
     * @return a number equal to sum of square mean deviation of each array values
     */
    public static function sumSquareMeanDeviation($arr)
    {
        $sum = 0;
        $num = count($arr);

        for ($i = 0; $i < $num; $i++) {$sum = $sum + ST::squareMeanDeviation($arr, $i);}
        return $sum;
    }

    /**
     *
     * @return a number equal to square mean deviation of each array values
     */
    public static function squareMeanDeviation($arr, $item)
    {
        return ST::meanDeviation($arr, $item) * ST::meanDeviation($arr, $item);
    }

    /**
     *
     * @return a number equal to sum of mean deviation of each array values
     */
    public static function sumMeanDeviation($arr)
    {
        $sum = 0;
        $num = count($arr);

        for ($i = 0; $i < $num; $i++) {$sum = $sum + ST::meanDeviation($arr, $i);}
        return $sum;
    }

    /**
     *
     * @return a number equal to mean deviation of each array values
     */
    public static function meanDeviation($arr, $item)
    {
        $average = ST::average($arr);return $arr[$item] - $average;
    }

    /**
     *
     * @return a number equal to mean of array values
     */
    public static function average($arr)
    {
        $sum = ST::sum($arr);
        $num = count($arr);return $sum / $num;
    }

    /**
     *
     * @return a number equal to sum of an array
     */
    public static function sum($arr)
    {
        return array_sum($arr);
    }

    /**
     *
     * @return an array of coefficients for 7 levels of volatilities
     */
    public static function getCoefParams($overall_market_coeff)
    {
        $daily_coef = 0.9 + ($overall_market_coeff / 10);

        $coefs = array(
            ConstEQ::LEVEL_VOLATILITY_COEF_1 * $daily_coef,
            ConstEQ::LEVEL_VOLATILITY_COEF_2 * $daily_coef,
            ConstEQ::LEVEL_VOLATILITY_COEF_3 * $daily_coef,
            ConstEQ::LEVEL_VOLATILITY_COEF_4 * $daily_coef,
            ConstEQ::LEVEL_VOLATILITY_COEF_5 * $daily_coef,
            ConstEQ::LEVEL_VOLATILITY_COEF_6 * $daily_coef,
            ConstEQ::LEVEL_VOLATILITY_COEF_7 * $daily_coef,
        );

        return $coefs;
    }

    /**
     * @return a binary true or false for is_numeric testing of an string
     */
    public static function isNumber($arr)
    {
        foreach ($arr as $b) {
            if (!is_numeric($b)) {
                return false;
            }
        }
        return true;
    }

}
\$\endgroup\$
  • 1
    \$\begingroup\$ I don't deal with such calculations in my day-to-day, so I won't be so bold to change any logic. I do find pow($variance, 3 / 2.0) to be a little weird. Is that a misplaced parenthesis or do you want $variance to the power of 1.5? \$\endgroup\$ – mickmackusa Apr 17 at 20:59
3
\$\begingroup\$
  • In getMean(), you don't need to call count() twice.

    public static function getMean($array) {
        return $array ? array_sum($array) / count($array) : ConstEQ::NEAR_ZERO_NUMBER;
    }
    
  • In getNormalize(), instead of subtracting $max from $min twice, I think it is easier to read $max != $min. Also, neither branch that calculates $normalized needs the outermost parentheses; the Order of Operations will ensure correct evaluation.

  • From PHP5.6+, pow() can be replaced with ** syntax. https://www.php.net/manual/en/function.pow.php This is more of a personal preference thing. There is something to be said for the clarity of pow().

  • In getStandardDeviation(), you are counting the input array over and over. You should ask php to count it once and save that value to a variable for future usage.

  • In getCovariance(), you are slicing the two arrays to a common length before iterating. This allows you to more simply use a foreach() versus a for() which relies on a count() call. Also I don't recommend hiding return on the right hand side of a if condition; just write it on the next line. $difA and $difB are single-use variables, so you could calculate everything after $add +=.

  • In getSkewness(), you can simplify pow($variance, 3 / 2.0) to pow($variance, 1.5).

  • In sumProductMeanDeviation(), you are calling count() to set up for(), otherwise it is not needed. Again, just use a foreach().

    foreach ($arr1 as $i => $notused) {
        $sum += ST::productMeanDeviation($arr1, $arr2, $i);
    }
    
  • In productMeanDeviation(), you don't need the outermost parentheses.

  • As @KIKOSoftware mentioned, mean() and average seem like mergable methods. I'll 2nd his advice to store reusable variables in the class so that php doesn't need to repeatedly count or sum anything more than once. DRY principle. This has been repeated in several reviews. We would like you to implement the advice from previous reviews before posting new scripts for review so that we don't have to repeat the same advice to the same user.

\$\endgroup\$
3
\$\begingroup\$

Trying to avoid duplicating any comments you have already had

  • Why name the class "ST"? It's not very clear to another developer what a class called ST actually does
  • Why extend EQ? Generally inheritance is use to make another class that provides the same functionality but more specialised, e.g. A "Car" might extend a class "Vehicle". I think it would be a good idea to keep these methods separate from EQ. If you wanted to use this class for Statistical Analysis for a different case other than the data in EQ, you would have to include everything from EQ just to access these methods.
  • Why implement ConstEQ? All of your usage of these constants seem to be by referring to "ConstEQ", this will work without "implements ConstEQ". The only reason you would implement ConstEQ is if you want to provide these constants for code which uses the ST class ( so they would call "ST::CONSTANT_NAME")
  • What if someone calls getMean with something other than an array? e.g. getMean('hello'); You can require an array by type hinting the parameter. function getMean(array $values)
  • You mean want to consider more thorough validation of the values passed to your functions, if you receive an array of strings when you expect an array of integers, you will have very strange behaviour, it would be better to throw an Exception in a case like that.
  • In PHP 7 you can hint on the return type of a method. It's useful as documentation for another developer but also means code which uses the method can rely on the type being returned without worrying about some failure case that returns something else. function getMean(array $values): int

  • When you have an if block which always returns you can actually skip the "else" to make code a bit simpler to read


    if (count($array) == 0) {
        return ConstEQ::NEAR_ZERO_NUMBER;
    }
    return array_sum($array) / count($array);

  • You could simplify getNormalize similarly

    if ($max - $min != 0) {
        return 2 * (($value - $min) / ($max - $min)) - 1;
    }
    return 2 * (($value - $min)) - 1;

  • The comment for isNumber implies you are accepting a string as a parameter but it looks like this actually accepts an array
\$\endgroup\$
1
\$\begingroup\$

This is also not something I deal with on a daily basis, but I have used it in the past, so I know what it's about.

Let me start with the biggest thing that immediately jumps out at me: You've put a lot of statistical methods in one class. Why? What do they have in common, apart from the fact that they are statistical methods? If I read your class the answer seems to be: Nothing. This would have worked fine if the methods were just independent function.

The point about statistical methods is that they deal with numbers; lots of them. Very often you don't only want to know the mean of a set, but also the median, the mode, etc. So, it would make sense to feed your statistical class an array of numbers and have methods to get information about that array. Like this:

class StatisticsOneDim // one dimensional statistics
{
    private $count;
    private $data;
    private $sum;
    private $mean;

    public function __construct($data)
    {
        $this->count = count($data);
        $this->data = $data;
    }

    public function getCount()
    {
        return $this->count;
    }

    public function getSum()
    {
        if (!isset($this->sum)) {
            $this->sum = array_sum($this->data);
        }
        return $this->sum;
    }

    public function getMean()
    {
        if (!isset($this->mean)) {
            $count = $this->getCount();
            if ($count == 0) $this->mean = false; // or generate an exception
                        else $this->mean = $this->getSum()/$count;
        }
        return $this->mean;
    }
}

Well, and so on. You can see what I am getting at. The data you want statistical information about is given to the class when it is created. After that you can interrogate the data. Once something like a sum or a mean has been computed it is stored in the class for quick retrieval later on. The count is used so often that I created it in the constructor.

OK, there might be methods that cannot be applied to the data in the class. For instance getCorrelation(). That method needs two data sets. So, it shouldn't be part of this class. I would create a separate class that can take two arrays, or even two StatisticsOneDim, and call it StatisticsTwoDims.

Does this help you further?

PS: I saw you have a getMean(), but also a average() method. These methods do the same, except the latter doesn't have 'division by zero' protection.

\$\endgroup\$
  • 1
    \$\begingroup\$ I updated the example code slightly. Now there's only one return per method. \$\endgroup\$ – KIKO Software Apr 18 at 9:16

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.