phit

Benchmarks and verification

OLS verification: R, statsmodels and PHP

This notebook fits exactly the same design matrices and outcomes with three implementations. The raw PHP extension and statsmodels receive the intercept explicitly; R’s formula below generates the same ones column. We use conventional covariance, Student t inference, 95% intervals, and Gaussian likelihood at the ML variance. R and Python are development references, not production dependencies.

See setup, verification report and math walkthrough. Run all cells to rebuild the evidence from your environment.

from pathlib import Path
import sys
import pandas as pd
ROOT = next(p for p in [Path.cwd(), *Path.cwd().parents] if (p / "Cargo.toml").exists())
sys.path.insert(0, str(ROOT / "docs/verifications"))
from compare_ols import cases, compare_case
simple = next(case for case in cases() if case['name'] == 'simulated_housing')
display(pd.DataFrame({'intercept': [row[0] for row in simple['x']], 'area_m2': [row[1] for row in simple['x']], 'monthly_rent': simple['y']}))
intercept area_m2 monthly_rent
0 1 74.4 1879.25
1 1 57.4 1805.03
2 1 66.4 2022.29
3 1 49.1 1267.85
4 1 45.7 1509.24
5 1 92.4 2240.10
6 1 37.0 959.51
7 1 93.9 2386.54

The displayed data are simulated apartments from the shared housing CSV. The math walkthrough explains the generating equation and sample correlations. The simulated_housing_multiple case also includes bedrooms: this predictor is strongly correlated with area, but the design remains full rank. Its generating coefficient is zero conditional on area; a sample estimate need not equal zero.

Equivalent calls

R, using the table above:

fit <- lm(monthly_rent ~ area_m2, data = observations, na.action = na.omit)
summary(fit)
confint(fit, level = 0.95)
logLik(fit)

Python statsmodels:

fit = sm.OLS(y, X, missing='drop', hasconst=True).fit(use_t=True)
fit.params, fit.bse, fit.tvalues, fit.pvalues, fit.conf_int(), fit.llf

PHP (after loading the extension):

$result = stats_ols(x: $x, y: $y); // missing defaults to 'drop'

The executable harness uses y ~ . - 1 in R with the explicit design columns to avoid inserting a second intercept. It calculates centered/uncentered R-squared explicitly because that formula is syntactically no-intercept even when the supplied matrix contains ones. All other quantities come from R’s fitted model. This makes the same runner work for multiple and no-intercept designs.

simple_result = compare_case(simple)
print(simple_result['versions'])
rows = []
for name in ['R', 'statsmodels', 'php-stats']:
    result = simple_result[name]
    for i, label in enumerate(['intercept', 'area_m2']):
        rows.append({'implementation': name, 'term': label, 'coefficient': result['coefficients'][i],
                     'SE': result['std_errors'][i], 't': result['t_statistics'][i], 'p': result['p_values'][i],
                     'lower 95%': result['conf_int'][i][0], 'upper 95%': result['conf_int'][i][1]})
display(pd.DataFrame(rows))
display(pd.DataFrame([{ 'implementation': name, **{key: simple_result[name][key] for key in
                     ['rss', 'sigma', 'df_residual', 'r_squared', 'adj_r_squared', 'log_likelihood']}}
                     for name in ['R', 'statsmodels', 'php-stats']]))
{'R': 'R version 4.6.1 (2026-06-24)', 'statsmodels': '0.15.0', 'numpy': '2.5.4', 'Python': '3.14.7', 'PHP': '8.5.10'}
implementation term coefficient SE t p lower 95% upper 95%
0 R intercept 372.478545 222.351403 1.67518 0.144919 -171.59574 916.552829
1 R area_m2 21.479724 3.293537 6.52178 0.000620 13.42073 29.538719
2 statsmodels intercept 372.478545 222.351403 1.67518 0.144919 -171.59574 916.552829
3 statsmodels area_m2 21.479724 3.293537 6.52178 0.000620 13.42073 29.538719
4 php-stats intercept 372.478545 222.351403 1.67518 0.144919 -171.59574 916.552829
5 php-stats area_m2 21.479724 3.293537 6.52178 0.000620 13.42073 29.538719
implementation rss sigma df_residual r_squared adj_r_squared log_likelihood
0 R 204471.403057 184.603811 6 0.876374 0.85577 -51.946476
1 statsmodels 204471.403057 184.603811 6 0.876374 0.85577 -51.946476
2 php-stats 204471.403057 184.603811 6 0.876374 0.85577 -51.946476

Automated agreement, including row deletion

The runner compares all statistical fields using abs(actual-reference) <= 1e-9 + 1e-7*abs(reference) and checks retained sample counts and row positions separately. It covers simple, multiple, correlated full-rank, and no-intercept cases. The missing-data case drops one predictor-missing row and one outcome-missing row: both implementations must use the same four observations. No warning text is silently lost: PHP warnings are captured and shown below.

R’s explicit na.omit and statsmodels’ explicit missing='drop' select complete cases. They do not imply that all libraries warn on deletion by default. PHP deliberately emits a warning and returns metadata.

results = [simple_result] + [compare_case(case) for case in cases() if case['name'] != 'simulated_housing']
summary = []
for result in results:
    largest_error = max(error for errors in result['max_absolute_errors_vs_R'].values() for error in errors.values())
    summary.append({'case': result['name'], 'n input': result['php-stats']['n_input'],
                    'n retained': result['php-stats']['n_observations'],
                    'n dropped': result['php-stats']['n_dropped'], 'largest absolute difference vs R': largest_error})
display(pd.DataFrame(summary))
missing = next(result for result in results if result['name'] == 'listwise_missing')
print('Dropped original positions:', missing['php-stats']['dropped_rows'])
print('Retained original positions:', missing['php-stats']['retained_rows'])
print('PHP warning:', missing['warnings'][0]['message'])
print('All three implementations agree for every case and checked field.')
case n input n retained n dropped largest absolute difference vs R
0 simulated_housing 8 8 0 3.492460e-10
1 simulated_housing_multiple 8 8 0 1.891749e-10
2 simple 5 5 0 1.065814e-13
3 multiple 10 10 0 1.776357e-14
4 no_intercept 10 10 0 4.263256e-14
5 correlated 10 10 0 2.870593e-12
6 listwise_missing 6 4 2 1.037392e-12
Dropped original positions: [1, 3]
Retained original positions: [0, 2, 4, 5]
PHP warning: stats_ols(): stats_ols dropped 2 of 6 observations with missing values (listwise deletion)
All three implementations agree for every case and checked field.

Read the limits of this evidence

The harness deliberately excludes perfect and zero fits from three-way finite comparisons: floating-point residuals can differ dramatically relative to their tiny magnitudes, and some inference is undefined. The Rust/PHP test suites cover those conventions separately. We also deliberately reject singular designs instead of asking each reference implementation to make its own aliasing decisions.

The tolerance includes an absolute floor; it does not promise relative accuracy for extremely small p-values. This notebook verifies these deterministic cases in the recorded environment, not every possible dataset or platform. See verification policy for coverage and fixture maintenance.

Sources: R lm, statsmodels OLS, statsmodels missing-data behavior.

From phpstats/phit@76b22f0.