$b[0]; }); // Initialize counters $nc = 0; // concordant pairs $nd = 0; // discordant pairs $ties_x = 0; // ties xᵢ = xⱼ $ties_y = 0; // ties yᵢ = yⱼ $ties_xy = 0; // ties xᵢ = xⱼ and yᵢ = yⱼ // Tally concordant, discordant, and tied pairs for ($i = 0; $i < $n; $i++) { for ($j = $i + 1; $j < $n; $j++) { // xᵢ = xⱼ and yᵢ = yⱼ -- neither concordant or discordant if ($xy[$i][self::X] == $xy[$j][self::X] && $xy[$i][self::Y] == $xy[$j][self::Y]) { $ties_xy++; // xᵢ = xⱼ -- neither concordant or discordant } elseif ($xy[$i][self::X] == $xy[$j][self::X]) { $ties_x++; // yᵢ = yⱼ -- neither concordant or discordant } elseif ($xy[$i][self::Y] == $xy[$j][self::Y]) { $ties_y++; // xᵢ < xⱼ and yᵢ < yⱼ -- concordant } elseif ($xy[$i][self::X] < $xy[$j][self::X] && $xy[$i][self::Y] < $xy[$j][self::Y]) { $nc++; // xᵢ > xⱼ and yᵢ < yⱼ or xᵢ < xⱼ and yᵢ > yⱼ -- discordant } else { $nd++; } } } // Numerator: (number of concordant pairs) - (number of discordant pairs) $⟮nc − nd⟯ = $nc - $nd; /* tau-a (no rank ties): * * nc - nd * τ = ---------- * n(n - 1)/2 */ if ($ties_x == 0 && $ties_y == 0) { return $⟮nc − nd⟯ / (($n * ($n - 1)) / 2); } /* tau-b (rank ties exist): * * nc - nd * τ = ----------------------------- * √(nc + nd + X₀)(nc + nd + Y₀) */ return $⟮nc − nd⟯ / sqrt(($nc + $nd + $ties_x) * ($nc + $nd + $ties_y)); } /** * ρ - Spearman's rank correlation coefficient (Spearman's rho) * * https://en.wikipedia.org/wiki/Spearman%27s_rank_correlation_coefficient * * 6 ∑ dᵢ² * ρ = 1 - --------- * n(n² − 1) * * Where * dᵢ: the difference between the two ranks of each observation * * @param array $X values for random variable X * @param array $Y values for random variable Y * * @return float * * @throws Exception\BadDataException if both random variables do not have the same number of elements */ public static function spearmansRho(array $X, array $Y): float { if (count($X) !== count($Y)) { throw new Exception\BadDataException('Both random variables must have the same number of elements'); } $n = count($X); // Sorted Xs and Ys $Xs = $X; $Ys = $Y; rsort($Xs); rsort($Ys); // Determine ranks of each X and Y // Some items might show up multiple times, so record each successive rank. $rg⟮X⟯ = []; $rg⟮Y⟯ = []; foreach ($Xs as $rank => $xᵢ) { if (!isset($rg⟮X⟯[$xᵢ])) { $rg⟮X⟯[$xᵢ] = []; } $rg⟮X⟯[$xᵢ][] = $rank; } foreach ($Ys as $rank => $yᵢ) { if (!isset($rg⟮Y⟯[$yᵢ])) { $rg⟮Y⟯[$yᵢ] = []; } $rg⟮Y⟯[$yᵢ][] = $rank; } // Determine average rank of each X and Y // Rank will not change if value only shows up once. // Average is for when values show up multiple times. $rg⟮X⟯ = array_map( function ($x) { return array_sum($x) / count($x); }, $rg⟮X⟯ ); $rg⟮Y⟯ = array_map( function ($y) { return array_sum($y) / count($y); }, $rg⟮Y⟯ ); // Difference between the two ranks of each observation $d = array_map( function ($x, $y) use ($rg⟮X⟯, $rg⟮Y⟯) { return abs($rg⟮X⟯[$x] - $rg⟮Y⟯[$y]); }, $X, $Y ); // Numerator: 6 ∑ dᵢ² $d² = Map\Single::square($d); $∑d² = array_sum($d²); // Denominator: n(n² − 1) $n⟮n² − 1⟯ = $n * ($n ** 2 - 1); /* * 6 ∑ dᵢ² * ρ = 1 - --------- * n(n² − 1) */ return 1 - ((6 * $∑d²) / $n⟮n² − 1⟯); } /** * Descriptive correlation report about two random variables * * @param array $X values for random variable X * @param array $Y values for random variable Y * @param bool $population Optional flag if all samples of a population are present * * @return array [cov, r, R2, tau, rho] * * @throws Exception\BadDataException * @throws Exception\OutOfBoundsException */ public static function describe(array $X, array $Y, bool $population = false): array { return [ 'cov' => self::covariance($X, $Y, $population), 'r' => self::r($X, $Y, $population), 'r2' => self::r2($X, $Y, $population), 'tau' => self::kendallsTau($X, $Y), 'rho' => self::spearmansRho($X, $Y), ]; } /** * Confidence ellipse (error ellipse) * Given the data in $X and $Y, create an ellipse * surrounding the data at $z standard deviations. * * The function will return $num_points pairs of X,Y data * http://stackoverflow.com/questions/3417028/ellipse-around-the-data-in-matlab * * @param array $X an array of independent data * @param array $Y an array of dependent data * @param float $z the number of standard deviations to encompass * @param int $num_points the number of points to include around the ellipse. The actual array * will be one larger because the first point and last will be repeated * to ease display. * * @return array paired x and y points on an ellipse aligned with the data provided * * @throws Exception\BadDataException * @throws Exception\BadParameterException * @throws Exception\IncorrectTypeException * @throws Exception\MatrixException * @throws Exception\OutOfBoundsException */ public static function confidenceEllipse(array $X, array $Y, float $z, int $num_points = 11): array { $standardNormal = new StandardNormal(); $p = 2 * $standardNormal->cdf($z) - 1; $chiSquared = new ChiSquared(2); $χ² = $chiSquared->inverse($p); $data_array[] = $X; $data_array[] = $Y; $data_matrix = new Matrix($data_array); $covarience_matrix = $data_matrix->covarianceMatrix(); // Scale the data by the confidence interval $cov = $covarience_matrix->scalarMultiply($χ²); $eigenvalues = Eigenvalue::closedFormPolynomialRootMethod($cov); // Sort the eigenvalues from highest to lowest rsort($eigenvalues); $V = Eigenvector::eigenvectors($cov, $eigenvalues); // Make ia diagonal matrix of the eigenvalues $D = MatrixFactory::diagonal($eigenvalues); $D = $D->map('sqrt'); $transformation_matrix = $V->multiply($D); $x_bar = Average::mean($X); $y_bar = Average::mean($Y); $translation_matrix = new Matrix([[$x_bar],[$y_bar]]); // We add a row to allow the transformation matrix to also traslate the ellipse to a different location $transformation_matrix = $transformation_matrix->augment($translation_matrix); $unit_circle = new Matrix(Trigonometry::unitCircle($num_points)); // We add a column of ones to allow us to translate the ellipse $unit_circle_with_ones = $unit_circle->augment(MatrixFactory::one($num_points, 1)); // The unit circle is rotated, stretched, and translated to the appropriate ellipse by the translation matrix. $ellipse = $transformation_matrix->multiply($unit_circle_with_ones->transpose())->transpose(); return $ellipse->getMatrix(); } }