markrogoyski/math-php
Math PHP is a standalone PHP library for advanced mathematics: linear algebra (matrices, vectors), statistics and probability distributions, numerical analysis, calculus, polynomials, finance, and more. It offers a clean, well-tested API for scientific and data-driven PHP applications.
Install with Composer — there is no service provider, no config file to publish, and no migrations. It is a pure-PHP computation library:
composer require markrogoyski/math-php
Everything lives under the MathPHP\ namespace and is usable immediately:
use MathPHP\Statistics\Descriptive;
use MathPHP\Statistics\Regression;
use MathPHP\LinearAlgebra\Matrix;
use MathPHP\LinearAlgebra\Vector;
use MathPHP\Probability\Distribution\Continuous\Normal;
Where to look first — orient by namespace, since the package is wide but shallow per area:
MathPHP\Statistics\Descriptive — mean, median, mode, variance, std dev, quartiles, skewness, kurtosis. This is the highest-value, lowest-risk entry point.MathPHP\Statistics\Regression — linear, power, exponential, logarithmic, polynomial fits with predict().MathPHP\Statistics\Correlation — Pearson/Spearman, covariance.MathPHP\Probability\Distribution\{Continuous,Discrete,Multivariate} — PDF/CDF/quantile for Normal, StudentT, Binomial, Poisson, etc.MathPHP\LinearAlgebra\{Matrix,Vector} — construct, multiply, transpose, invert, solve, eigenvalues, and the new column scalar ops.MathPHP\NumericalAnalysis — root finding, interpolation, integration when you need it.First realistic use case in a Laravel app: a reporting or dashboard feature that needs descriptive stats over a collection. Pull values out of Eloquent with pluck()->all(), hand the plain array to Descriptive::mean, Descriptive::median, Descriptive::sampleStandardDeviation, and render. That single step replaces hand-rolled aggregation code and gets you edge-case handling (empty arrays, zero variance) for free — though, per the gotchas below, many of those edge cases now throw rather than return sentinels.
A good second step is Normal::cdf() for percentile scoring, or Matrix for a small feature-engineering or scoring pipeline.
Wrap it in your own service, don't call it from controllers. The package is stateless and global-namespaced; wrap the handful of functions you actually use behind an app-level class (App\Services\Math\StatisticsService) or a small façade. This gives you one place to adapt when upstream ships behavioral changes, one place to add input validation, and a seam to swap in fixed fixtures during tests.
final class StatisticsService
{
public function summarize(array $values): array
{
$values = array_values(array_filter($values, 'is_numeric'));
if (count($values) < 2) {
return ['count' => count($values), 'mean' => $values[0] ?? null];
}
return [
'count' => count($values),
'mean' => Descriptive::mean($values),
'median' => Descriptive::median($values),
'sd' => Descriptive::sampleStandardDeviation($values),
'min' => min($values),
'max' => max($values),
];
}
}
Validate inputs before you call, not after. Given v2.13.0's shift toward throwing on degenerate input, the pattern is: guard for empty/short/constant datasets in your wrapper, then let the library compute on clean data. Treat BadDataException and OutOfBoundsException as signals your guard was incomplete, not as normal control flow.
Use matrices for scoring and feature pipelines. The new columnAddScalar, columnSubtract, columnSubtractScalar, and columnDivide are useful for normalizing or centering a feature matrix in place:
$m = new Matrix($rows); // e.g. feature vectors per user
$centered = $m->columnSubtract(new Vector($columnMeans->asArray()));
$scaled = $centered->columnDivide(new Vector($columnSds->asArray()));
Prefer this over nested array_map loops — it keeps the numeric code legible and consistent.
Push heavy computation to queued jobs. Matrix inversion, eigenvalue decomposition, and noncentral distribution functions (notably NoncentralT::median) are CPU-heavy in PHP. Anything operating on more than a few hundred rows, or computing many distributions in a loop, belongs in a job — not a request cycle. Cache the result keyed by input hash if users request the same report repeatedly.
Use regression predict for forecasting features. Regression\Linear::predict($x) and friends give you a fitted model + prediction in one object, which is a clean replacement for a Python microservice on simple forecasting use cases. Persist the regression parameters (not the object) if you need to reuse the fit.
Add golden-file tests around every numeric output. Any feature that surfaces numbers to users — reports, dashboards, risk scores, A/B test results — should have a snapshot test that asserts the exact numeric output. This is your only reliable defence against silent numerical drift on future upgrades, because signature-compatible functions can and do change their results between minor versions.
Pin the exact version; do not use a caret range. v2.13.0 is a minor release that contains functionally breaking changes (renames, new exceptions, changed numerics). The maintainers apply semver loosely. Use "markrogoyski/math-php": "2.13.0" and review the changelog before every bump.
SampleData\People::getShowSize() was renamed to getShoeSize(). A hard rename — any remaining caller gets a fatal error, but only when that code path executes. If you use the SampleData namespace at all (often in tests), grep for it before upgrading.
Many methods that used to return NaN, INF, 0, or [] now throw. Confirmed in v2.13.0: sampleCovariance, populationCorrelationCoefficient, sampleCorrelationCoefficient, cohensQ, hedgesG, cohensD, glassDelta, kullbackLeibler, jensenShannon, stemAndLeafPlot, grubbsCriticalValue, and several Statistics\Experiment functions. Audit any call site that relied on sentinel returns — in a request you get a 500, and in a job you get retries that may poison the queue. Wrap at the boundary and decide per call site whether to fail, degrade, or fall back.
Silent numeric drift is the higher-severity risk. v2.13.0 changed results without changing signatures in:
Statistics\Distribution::fractionalRanking (rewritten tie detection — previously grouped by strval(), now uses index tracking and ==)cumulativeFrequency / cumulativeRelativeFrequency (corrected to frequency-distribution-over-sorted-data semantics)Probability\Distribution\Discrete\Hypergeometric::cdf (now sums PMFs directly)Descriptive::geometricMean (rewritten to log-space)None of these will throw. Your tests will pass if they only assert is_numeric(). Snapshot the actual values.
Return shapes can change even when no exception is raised. Algebra::quadratic with a=0, b=0 now returns [] instead of dividing by zero; Trigonometry::unitCircle guards a single-point case. If downstream code assumed a non-empty result or a specific array shape, it will fail differently.
coefficientOfVariation returns NAN when the mean is zero — this is one of the few cases where a sentinel is still returned rather than thrown. Watch for NAN propagation into your formatting layer.
PHP arrays will be your performance wall on matrices. Memory overhead per element is significant. A matrix that fits comfortably in NumPy will not necessarily fit comfortably here. Benchmark before shipping anything that constructs matrices from large result sets, and consider chunking feature pipelines.
Guard degenerate inputs explicitly — empty arrays, single-element datasets, zero variance. These now raise in many places (Grubbs needs ≥3 observations; standard deviation of zero raises; negative values in stemAndLeafPlot raise). Validating in your wrapper is cheaper than catching exceptions on the hot path.
Extension points are limited but sufficient. There is no plugin system. The practical extension pattern is compositional: your service class owns the calls, your golden-file tests own the contract, and a fork is cheap if upstream stalls — the code is plain PHP, and the surface area you actually use (descriptive stats, a few distributions, basic matrix ops) is small enough to reimplement if needed. Document which functions you depend on so the fork decision stays cheap.
How can I help you explore Laravel packages today?