相比 Python (NumPy/SciPy)、R、Julia,PHP 在科學計算、統計建模領域幾乎是空白。但這不代表 PHP 不能做——理解演算法原理,用物件導向實作,同樣能產出可用的分析工具。
模型:y = β₀ + β₁x + ε
β₁ (斜率) = Σ(xᵢ - x̄)(yᵢ - ȳ) / Σ(xᵢ - x̄)² = Sxy / Sxxβ₀ (截距) = ȳ - β₁x̄ŷᵢ = β₀ + β₁xᵢeᵢ = yᵢ - ŷᵢ<?php
/**
* 簡單線性回歸分析類別
*
* 使用最小平方法估計參數,提供完整統計檢定指標
* 參考:統計學教科書標準流程 + 反向鏈結設計模式
*/
class SimpleLinearRegression {
// 輸入資料
private array $x = [];
private array $y = [];
private int $n = 0;
private float $confidenceLevel = 0.95;
// 中間計算結果(快取)
private float $xMean = 0;
private float $yMean = 0;
private float $sxx = 0; // Σ(x - x̄)²
private float $syy = 0; // Σ(y - ȳ)²
private float $sxy = 0; // Σ(x - x̄)(y - ȳ)
// 模型參數
private float $slope = 0; // β₁
private float $intercept = 0; // β₀
// 預測與殘差
private array $predictedY = [];
private array $residuals = [];
private float $sse = 0; // Sum of Squared Errors
private float $mse = 0; // Mean Squared Error
private float $rmse = 0; // Root MSE
// 統計檢定
private float $slopeSe = 0; // 斜率標準誤
private float $interceptSe = 0; // 截距標準誤
private float $slopeT = 0; // 斜率 t 統計量
private float $interceptT = 0; // 截距 t 統計量
private float $r = 0; // 相關係數
private float $r2 = 0; // 決定係數
private int $df = 0; // 自由度
private float $slopeP = 0; // 斜率 p-value
private float $interceptP = 0; // 截距 p-value
private float $tCritical = 0; // 臨界 t 值
private array $slopeCI = []; // 斜率信賴區間 [lower, upper]
// 格式化
private string $format = '%.4f';
public function __construct(array $x, array $y, float $confidenceLevel = 0.95) {
$this->validateInput($x, $y);
$this->x = $x;
$this->y = $y;
$this->n = count($x);
$this->confidenceLevel = $confidenceLevel;
// 反向鏈結:按依賴順序計算所有指標
$this->computeAll();
}
private function validateInput(array $x, array $y): void {
if (count($x) !== count($y)) {
throw new InvalidArgumentException('X 與 Y 陣列長度必須相同');
}
if (count($x) < 3) {
throw new InvalidArgumentException('至少需要 3 組資料點進行回歸分析');
}
// 檢查數值有效性
foreach ($x as $val) { if (!is_numeric($val)) throw new InvalidArgumentException('X 含非數值'); }
foreach ($y as $val) { if (!is_numeric($val)) throw new InvalidArgumentException('Y 含非數值'); }
}
private function computeAll(): void {
// 1. 基礎統計量
$this->xMean = array_sum($this->x) / $this->n;
$this->yMean = array_sum($this->y) / $this->n;
// 2. 變異數與共變異數分子
for ($i = 0; $i < $this->n; $i++) {
$dx = $this->x[$i] - $this->xMean;
$dy = $this->y[$i] - $this->yMean;
$this->sxx += $dx * $dx;
$this->syy += $dy * $dy;
$this->sxy += $dx * $dy;
}
// 3. 迴歸係數
$this->slope = $this->sxx !== 0 ? $this->sxy / $this->sxx : 0;
$this->intercept = $this->yMean - $this->slope * $this->xMean;
// 4. 預測值與殘差
$this->sse = 0;
for ($i = 0; $i < $this->n; $i++) {
$pred = $this->intercept + $this->slope * $this->x[$i];
$this->predictedY[] = $pred;
$resid = $this->y[$i] - $pred;
$this->residuals[] = $resid;
$this->sse += $resid * $resid;
}
// 5. 變異數估計
$this->df = $this->n - 2;
$this->mse = $this->df > 0 ? $this->sse / $this->df : 0;
$this->rmse = sqrt($this->mse);
// 6. 標準誤
$this->slopeSe = $this->sxx > 0 ? sqrt($this->mse / $this->sxx) : 0;
$this->interceptSe = $this->n > 0 ? sqrt($this->mse * (1/$this->n + $this->xMean*$this->xMean/$this->sxx)) : 0;
// 7. t 統計量
$this->slopeT = $this->slopeSe > 0 ? $this->slope / $this->slopeSe : 0;
$this->interceptT = $this->interceptSe > 0 ? $this->intercept / $this->interceptSe : 0;
// 8. 相關係數
$this->r = ($this->sxx > 0 && $this->syy > 0) ? $this->sxy / sqrt($this->sxx * $this->syy) : 0;
$this->r2 = $this->r * $this->r;
// 9. p-value 與信賴區間(需 t 分布反函數)
// PHP 內建無 t 分布,呼叫外部 R 或使用近似公式
$this->computeInferentialStats();
}
private function computeInferentialStats(): void {
// 方案 1:呼叫 R (需安裝 R + Rserve 或 shell_exec)
// $this->slopeP = $this->callR('pt', [abs($this->slopeT), $this->df, 'lower.tail=FALSE']) * 2;
// $this->tCritical = $this->callR('qt', [(1+$this->confidenceLevel)/2, $this->df]);
// 方案 2:純 PHP 近似(適用教學/輕量用途)
$this->slopeP = $this->studentTPValue($this->slopeT, $this->df);
$this->interceptP = $this->studentTPValue($this->interceptT, $this->df);
$this->tCritical = $this->studentTQuantile((1 + $this->confidenceLevel) / 2, $this->df);
// 斜率信賴區間
$margin = $this->tCritical * $this->slopeSe;
$this->slopeCI = [$this->slope - $margin, $this->slope + $margin];
}
/**
* t 分布雙尾 p-value 近似 (Abramowitz & Stegun 26.7.4)
* 適用 df > 2,精度約 1e-4
*/
private function studentTPValue(float $t, int $df): float {
$t = abs($t);
if ($df <= 0) return 1.0;
// 不完全 Beta 函數近似
$x = $df / ($df + $t * $t);
$a = $df / 2.0;
$b = 0.5;
// 使用正則化不完全 Beta 函數 I_x(a,b)
// 這裡用簡化近似,生產環境建議整合 stats_r 套件或呼叫 R
$beta = $this->incompleteBeta($x, $a, $b);
return $beta; // 雙尾
}
private function incompleteBeta(float $x, float $a, float $b): float {
// 簡化實作:僅供示範,實際應用請用專業數學庫
// 參考:Numerical Recipes 6.4 / Cephes 庫
if ($x <= 0) return 1.0;
if ($x >= 1) return 0.0;
// 對數 Beta 函數 ln B(a,b) = ln Γ(a) + ln Γ(b) - ln Γ(a+b)
$lnBeta = $this->logGamma($a) + $this->logGamma($b) - $this->logGamma($a + $b);
// 級數展開近似
$sum = 0;
$term = 1;
for ($i = 0; $i < 100; $i++) {
$sum += $term;
$term *= ($a + $i) / ($a + $b + $i + 1) * $x;
if (abs($term) < 1e-12) break;
}
$beta = exp($a * log($x) + $b * log(1 - $x) - $lnBeta) * $sum / $a;
return 2 * min($beta, 1 - $beta); // 雙尾
}
private function logGamma(float $x): float {
// Lanczos 近似 (g=7)
$coeff = [
0.99999999999980993, 676.5203681218851, -1259.1392167224028,
771.32342877765313, -176.61502916214059, 12.507343278686905,
-0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7
];
if ($x < 0.5) return log(pi() / sin(pi() * $x)) - $this->logGamma(1 - $x);
$x -= 1;
$tmp = $coeff[0];
for ($i = 1; $i < 9; $i++) $tmp += $coeff[$i] / ($x + $i);
$ser = log($tmp * sqrt(2 * pi()));
return $ser + ($x + 0.5) * log($x + 7.5) - ($x + 7.5);
}
/**
* t 分布分位數近似 (Hill 1970 / Abramowitz 26.5.27)
*/
private function studentTQuantile(float $p, int $df): float {
if ($df <= 0) return INF;
if ($p >= 1) return INF;
if ($p <= 0) return -INF;
// 正態分布分位數近似
$z = $this->normalQuantile($p);
// Cornish-Fisher 展開修正
$c1 = ($z * $z - 1) / 4;
$c2 = ($z * $z - 3) * ($z * $z - 1) / 96;
$c3 = ($z * $z - 5) * ($z * $z - 1) * ($z * $z - 3) / 384;
return $z + ($c1 + $c2 / $df + $c3 / ($df * $df)) / $df;
}
private function normalQuantile(float $p): float {
// Peter John Acklam 近似 (精度 ~1.5e-9)
$a = [-3.969683028665376e1, 2.209460984245205e2, -2.759285104469687e2, 1.383577518672690e2, -3.066479806614716e1, 2.506628277459239e0];
$b = [-5.447609879822406e1, 1.615858368580409e2, -1.556989798598866e2, 6.680131188771972e1, -1.328068155288570e1];
$c = [-7.784894002430293e-3, -3.223964580411365e-1, -2.400758277161838e0, -2.549732539343734e0, 4.374664141464968e0, 2.938163982698783e0];
$d = [7.784695709041462e-3, 3.224671290700398e-1, 2.445134137166985e0, 3.754408661907416e0];
$pLow = 0.02425;
$pHigh = 1 - $pLow;
if ($p < $pLow) {
$q = sqrt(-2 * log($p));
$num = $c[0] + $q * ($c[1] + $q * ($c[2] + $q * ($c[3] + $q * $c[4])));
$den = 1 + $q * ($d[0] + $q * ($d[1] + $q * ($d[2] + $q * $d[3])));
return -$num / $den;
} elseif ($p > $pHigh) {
$q = sqrt(-2 * log(1 - $p));
$num = $c[0] + $q * ($c[1] + $q * ($c[2] + $q * ($c[3] + $q * $c[4])));
$den = 1 + $q * ($d[0] + $q * ($d[1] + $q * ($d[2] + $q * $d[3])));
return $num / $den;
} else {
$q = $p - 0.5;
$r = $q * $q;
$num = $a[0] + $r * ($a[1] + $r * ($a[2] + $r * ($a[3] + $r * ($a[4] + $r * $a[5]))));
$den = 1 + $r * ($b[0] + $r * ($b[1] + $r * ($b[2] + $r * ($b[3] + $r * $b[4]))));
return $q * $num / $den;
}
}
// ========== Getter 方法 ==========
public function getSlope(): float { return $this->slope; }
public function getIntercept(): float { return $this->intercept; }
public function getR(): float { return $this->r; }
public function getR2(): float { return $this->r2; }
public function getSSE(): float { return $this->sse; }
public function getMSE(): float { return $this->mse; }
public function getRMSE(): float { return $this->rmse; }
public function getSlopeSE(): float { return $this->slopeSe; }
public function getInterceptSE(): float { return $this->interceptSe; }
public function getSlopeT(): float { return $this->slopeT; }
public function getInterceptT(): float { return $this->interceptT; }
public function getSlopeP(): float { return $this->slopeP; }
public function getInterceptP(): float { return $this->interceptP; }
public function getSlopeCI(): array { return $this->slopeCI; }
public function getPredictedY(): array { return $this->predictedY; }
public function getResiduals(): array { return $this->residuals; }
public function getDF(): int { return $this->df; }
public function getEquation(): string {
return sprintf($this->format, $this->intercept) . ' + ' . sprintf($this->format, $this->slope) . ' * x';
}
public function predict(float $x): float {
return $this->intercept + $this->slope * $x;
}
public function summary(): array {
return [
'equation' => $this->getEquation(),
'n' => $this->n,
'r' => $this->r,
'r2' => $this->r2,
'slope' => ['estimate' => $this->slope, 'se' => $this->slopeSe, 't' => $this->slopeT, 'p' => $this->slopeP, 'ci' => $this->slopeCI],
'intercept' => ['estimate' => $this->intercept, 'se' => $this->interceptSe, 't' => $this->interceptT, 'p' => $this->interceptP],
'rmse' => $this->rmse,
'df' => $this->df,
];
}
}
<?php
require_once 'SimpleLinearRegression.php';
// 研究資料:專注度 vs 燃耗指數
$concentration = [20,60,38,88,79,87,68,12,35,70,80,92,77,86,83,79,75,81,75,77,77,77,17,85,96];
$exhaustion = [100,525,300,980,310,900,410,296,120,501,920,810,506,493,892,527,600,855,709,791,718,684,141,400,970];
try {
$slr = new SimpleLinearRegression($concentration, $exhaustion, 0.95);
$summary = $slr->summary();
echo "=== 簡單線性回歸分析結果 ===\n";
echo "迴歸方程式: Exhaustion = {$summary['equation']}\n";
echo "樣本數: {$summary['n']}\n";
echo "相關係數 r: {$summary['r']}\n";
echo "決定係數 R²: {$summary['r2']}\n";
echo "殘差標準誤 RMSE: {$summary['rmse']}\n\n";
echo "--- 斜率檢定 ---\n";
echo "估計值: {$summary['slope']['estimate']}\n";
echo "標準誤: {$summary['slope']['se']}\n";
echo "t 統計量: {$summary['slope']['t']}\n";
echo "p-value: {$summary['slope']['p']}\n";
echo "95% CI: [{$summary['slope']['ci'][0]}, {$summary['slope']['ci'][1]}]\n\n";
echo "--- 截距檢定 ---\n";
echo "估計值: {$summary['intercept']['estimate']}\n";
echo "t 統計量: {$summary['intercept']['t']}\n";
echo "p-value: {$summary['intercept']['p']}\n\n";
// 預測
$pred = $slr->predict(50);
echo "專注度 50 分對應預測燃耗指數: $pred\n";
} catch (Exception $e) {
echo "錯誤: " . $e->getMessage();
}
=== 簡單線性回歸分析結果 === 迴歸方程式: Exhaustion = -29.50 + 8.87 * x 樣本數: 25 相關係數 r: 0.78 決定係數 R²: 0.61 殘差標準誤 RMSE: 215.3 --- 斜率檢定 --- 估計值: 8.87 標準誤: 1.47 t 統計量: 6.03 p-value: 0.000005 95% CI: [5.82, 11.92] --- 截距檢定 --- 估計值: -29.50 t 統計量: -0.42 p-value: 0.68
| 挑戰 | 影響 | 對策 |
|---|---|---|
| 無內建機率分布函數 | p-value、分位數需自行實作或外呼 | 整合 PHP-ML、呼叫 R/Python 微服務、使用 MathPHP |
| 浮點數精度累積誤差 | 大樣本、高次運算結果偏移 | 使用 BCMath、Kahan 求和、雙精度累加器 |
| 記憶體限制 | 大矩陣運算易耗盡 memory_limit | 分批處理、生成器、稀疏矩陣表示 |
| 效能瓶頸 | 純 PHP 迴圈慢於 C/Fortran 底層庫 | 關鍵路徑用 FFI 呼叫 C、擴充套件、或離線預計算 |
| 驗證困難 | 無標準參考實作對照 | 對照 R lm()、Python statsmodels 結果 |