[PECL-CVS] [pecl-math-stats] master: Merge pull request #16 from php/fix/stats_stat_correlation

[email protected] (Rasmus Lerdorf via GitHub) Tue, 21 Apr 2026 03:27:36 +0000
Newsgroups php.pecl.cvs
Message-ID <[email protected]>
Author: Rasmus Lerdorf (rlerdorf)
Committer: GitHub (web-flow)
Pusher: rlerdorf
Date: 2026-04-21T04:27:34+01:00

Commit: https://github.com/php/pecl-math-stats/commit/d81618d3eeb3d29bcd3c02ac29678baac2739484
Raw diff: https://github.com/php/pecl-math-stats/commit/d81618d3eeb3d29bcd3c02ac29678baac2739484.diff

Merge pull request #16 from php/fix/stats_stat_correlation

Fix stats_stat_correlation() returning near-zero residuals on ARM64

Changed paths:
  A  tests/stats_stat_correlation_degenerate.phpt
  M  package.xml
  M  php_stats.c


Diff:

diff --git a/package.xml b/package.xml
index 65e50cb..56f93ef 100644
--- a/package.xml
+++ b/package.xml
@@ -125,6 +125,7 @@ http://pear.php.net/dtd/package-2.0.xsd">
     <file name="stats_standard_deviation.phpt" role="test" />
     <file name="stats_stat_binomial_coef.phpt" role="test" />
     <file name="stats_stat_correlation.phpt" role="test" />
+    <file name="stats_stat_correlation_degenerate.phpt" role="test" />
     <file name="stats_stat_factorial.phpt" role="test" />
     <file name="stats_stat_independent_t.phpt" role="test" />
     <file name="stats_stat_innerproduct.phpt" role="test" />
diff --git a/php_stats.c b/php_stats.c
index 5846efb..88f101b 100644
--- a/php_stats.c
+++ b/php_stats.c
@@ -3260,14 +3260,12 @@ PHP_FUNCTION(stats_stat_correlation)
 	int ynum = 0;
 	double sx  = 0.0;
 	double sy  = 0.0;
-	double sxx = 0.0;
-	double syy = 0.0;
-	double sxy = 0.0;
 	double mx;
 	double my;
-	double vx;
-	double vy;
-	double cc;
+	double vx  = 0.0;
+	double vy  = 0.0;
+	double cc  = 0.0;
+	double dx, dy;
 	double rr;
 
 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "z/z/", &arg1, &arg2) == FAILURE) {
@@ -3285,6 +3283,12 @@ PHP_FUNCTION(stats_stat_correlation)
 		RETURN_FALSE;
 	}
 
+	if (xnum < 2) {
+		php_error_docref(NULL, E_WARNING, "Correlation requires at least 2 data points");
+		RETURN_FALSE;
+	}
+
+	/* First pass: compute means */
 	zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(arg1), &pos1);
 	zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(arg2), &pos2);
 
@@ -3294,11 +3298,8 @@ PHP_FUNCTION(stats_stat_correlation)
 		convert_to_double_ex(data1);
 		convert_to_double_ex(data2);
 
-		sx  += Z_DVAL_P(data1);
-		sxx += Z_DVAL_P(data1) * Z_DVAL_P(data1);
-		sy  += Z_DVAL_P(data2);
-		syy += Z_DVAL_P(data2) * Z_DVAL_P(data2);
-		sxy += Z_DVAL_P(data1) * Z_DVAL_P(data2);
+		sx += Z_DVAL_P(data1);
+		sy += Z_DVAL_P(data2);
 
 		zend_hash_move_forward_ex(Z_ARRVAL_P(arg1), &pos1);
 		zend_hash_move_forward_ex(Z_ARRVAL_P(arg2), &pos2);
@@ -3306,11 +3307,38 @@ PHP_FUNCTION(stats_stat_correlation)
 
 	mx = sx / xnum;
 	my = sy / ynum;
-	vx = sxx - (xnum * mx * mx);
-	vy = syy - (ynum * my * my);
-	cc = sxy - (xnum * mx * my);
+
+	/* Second pass: compute variance and covariance from deviations */
+	zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(arg1), &pos1);
+	zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(arg2), &pos2);
+
+	while ((data1 = zend_hash_get_current_data_ex(Z_ARRVAL_P(arg1), &pos1)) != NULL
+			&& (data2 = zend_hash_get_current_data_ex(Z_ARRVAL_P(arg2), &pos2)) != NULL) {
+
+		convert_to_double_ex(data1);
+		convert_to_double_ex(data2);
+
+		dx = Z_DVAL_P(data1) - mx;
+		dy = Z_DVAL_P(data2) - my;
+
+		vx += dx * dx;
+		vy += dy * dy;
+		cc += dx * dy;
+
+		zend_hash_move_forward_ex(Z_ARRVAL_P(arg1), &pos1);
+		zend_hash_move_forward_ex(Z_ARRVAL_P(arg2), &pos2);
+	}
+
+	if (vx == 0.0 || vy == 0.0) {
+		php_error_docref(NULL, E_WARNING, "Correlation is undefined when one or both arrays have zero variance");
+		RETURN_FALSE;
+	}
+
 	rr = cc / sqrt(vx * vy);
 
+	if (rr > 1.0) rr = 1.0;
+	if (rr < -1.0) rr = -1.0;
+
 	RETURN_DOUBLE(rr);
 }
 /* }}} */
diff --git a/tests/stats_stat_correlation_degenerate.phpt b/tests/stats_stat_correlation_degenerate.phpt
new file mode 100644
index 0000000..493d0ab
--- /dev/null
+++ b/tests/stats_stat_correlation_degenerate.phpt
@@ -0,0 +1,28 @@
+--TEST--
+stats_stat_correlation() degenerate inputs
+--FILE--
+<?php
+// empty arrays
+var_dump(stats_stat_correlation(array(), array()));
+
+// single-element arrays
+var_dump(stats_stat_correlation(array(1), array(2)));
+
+// zero variance in X
+var_dump(stats_stat_correlation(array(1, 1, 1), array(1, 2, 3)));
+
+// zero variance in Y
+var_dump(stats_stat_correlation(array(1, 2, 3), array(5, 5, 5)));
+?>
+--EXPECTF--
+Warning: stats_stat_correlation(): Correlation requires at least 2 data points in %s on line %d
+bool(false)
+
+Warning: stats_stat_correlation(): Correlation requires at least 2 data points in %s on line %d
+bool(false)
+
+Warning: stats_stat_correlation(): Correlation is undefined when one or both arrays have zero variance in %s on line %d
+bool(false)
+
+Warning: stats_stat_correlation(): Correlation is undefined when one or both arrays have zero variance in %s on line %d
+bool(false)