[PECL-CVS] [pecl-math-stats] fix/stats_stat_correlation: Fix stats_stat_correlation() returning near-zero residuals on ARM64
[email protected] (Rasmus Lerdorf) Tue, 21 Apr 2026 03:04:57 +0000
| Newsgroups | php.pecl.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: Rasmus Lerdorf (rlerdorf)
Date: 2026-04-20T23:04:41-04:00
Commit: https://github.com/php/pecl-math-stats/commit/65a16409cb346c4667d79a2c2dd02bb74e78a5f6
Raw diff: https://github.com/php/pecl-math-stats/commit/65a16409cb346c4667d79a2c2dd02bb74e78a5f6.diff
Fix stats_stat_correlation() returning near-zero residuals on ARM64
Changed paths:
A tests/stats_stat_correlation_degenerate.phpt
M php_stats.c
Diff:
diff --git a/php_stats.c b/php_stats.c
index 5846efb..0e63488 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,35 @@ 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) {
+
+ 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..f51d506
--- /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([], []));
+
+// single-element arrays
+var_dump(stats_stat_correlation([1], [2]));
+
+// zero variance in X
+var_dump(stats_stat_correlation([1, 1, 1], [1, 2, 3]));
+
+// zero variance in Y
+var_dump(stats_stat_correlation([1, 2, 3], [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)