Regression Diagnostics¶
Tools for detecting multicollinearity, quasi-separation, influence and outliers, plus residual batteries for OLS and GLM fits.
Augment (per-observation diagnostics)¶
augment¶
One combined, row-aligned expression returning R broom::augment-style per-observation
diagnostics for any of nine supported models — an alternative to composing the individual
OLS residual/leverage/Cook's-distance functions below, and the only way to get these
diagnostics for a GLM fit in one call.
ps.augment(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
model: str = "ols",
weights: Union[pl.Expr, str, None] = None,
residual_type: str | None = None,
add_intercept: bool = True,
theta: float | None = None,
var_power: float = 1.5,
link_power: float | None = None,
max_iter: int = 100,
tol: float = 1e-8,
) -> pl.Expr
Parameters:
- model (default "ols") — one of "ols", "wls", "logistic", "probit", "cloglog",
"poisson", "negative_binomial", "tweedie", "gamma".
- weights — prior weights; required (and only valid) with model="wls".
- residual_type — "response", "pearson", or "deviance". Defaults to "response" for
ols/wls and "deviance" for every GLM (matching R's broom::augment).
- add_intercept (default True).
- theta — fixed negative-binomial dispersion (model="negative_binomial" only); None
estimates it from the data.
- var_power (default 1.5) — Tweedie variance power (model="tweedie" only): 0 Gaussian,
1 Poisson, 2 Gamma, 3 inverse Gaussian.
- link_power (default None) — Tweedie link power (model="tweedie" only). None uses the
canonical link (1 - var_power), matching ps.tweedie() and Tweedie(link_power=None) — the
same model the user fits with the same arguments. Pass an explicit value (e.g. 0.0 for the
log link) to override it.
- max_iter/tol — IRLS iteration limit/tolerance (GLMs only).
Returns: Struct{fitted, residual, std_residual, stud_residual, leverage, cooks_d, dffits}
— see Augment Output for the R-name mapping
(std_residual = rstandard, stud_residual = rstudent, leverage = hatvalues,
cooks_d = cooks.distance). One row per input row (returns_scalar=False, like the
*_predict expressions).
NA handling: rows with a null/non-finite y, x, or (for wls) weights entry come back
NaN in every field; the remaining rows equal a fit computed without that row — this is not an
error condition, it is standard broom behaviour (a dropped observation legitimately changes
every remaining row's leverage/Cook's distance/etc.).
R equivalent: broom::augment(model, type.predict = residual_type, type.residuals = residual_type).
Cross-check: for model="ols", augment's leverage/cooks_d/dffits/std_residual/
stud_residual agree elementwise with ps.leverage/ps.cooks_distance/ps.dffits/
ps.studentized_residuals/ps.externally_studentized_residuals below.
PyModel equivalent: every model listed above also has a .augment(x, y, residual_type=None)
method on its PyModel class (WLS.augment additionally takes weights=None, defaulting to the
fit's own weights) — see Model Classes and
GLM Model Classes. It returns the same seven fields as a dict of numpy
arrays, plus a trailing resid_type key (the resolved residual_type string), and always
describes exactly what that PyModel instance was fit with.
Example:
import numpy as np
import polars as pl
import polars_statistics as ps
np.random.seed(0)
n = 50
x1 = np.random.normal(0, 1, n)
y = 1.0 + 2.0 * x1 + np.random.normal(0, 0.5, n)
df = pl.DataFrame({"y": y, "x1": x1})
result = df.select(ps.augment("y", "x1", model="ols").alias("aug")).unnest("aug")
print(result.head(3))
# Per-group diagnostics
grouped = pl.DataFrame({"group": ["A"] * n, "y": y, "x1": x1}).group_by("group").agg(
ps.augment("y", "x1", model="ols").alias("aug")
)
condition_number¶
Compute condition number diagnostics to detect multicollinearity in design matrices.
Returns: See Condition Number Output
Example:
Interpretation:
| Condition Number | Severity | Recommendation |
|---|---|---|
| < 30 | Well-conditioned | Numerically stable |
| 30 - 100 | Moderate | Monitor for instability |
| 100 - 1000 | High | Consider regularization or removing predictors |
| > 1000 | Severe | Strong multicollinearity present |
check_binary_separation¶
Detect quasi-separation in binary response data (logistic/probit regression).
ps.check_binary_separation(
y: Union[pl.Expr, str], # Binary (0/1)
*x: Union[pl.Expr, str],
) -> pl.Expr
Returns: See Separation Check Output
Example:
Separation Types:
- Complete: Predictor perfectly divides the classes (MLE does not exist)
- Quasi: Nearly perfect separation with 1-2 observations crossing
- MonotonicResponse: Each predictor level has all observations in same class
When Separation is Detected:
- Consider using penalized regression (lambda_ > 0)
- Remove or combine problematic predictors
- Use Firth's bias-reduced logistic regression (if available)
check_count_sparsity¶
Detect sparsity-induced separation in count data (Poisson/NegBin regression).
ps.check_count_sparsity(
y: Union[pl.Expr, str], # Non-negative counts
*x: Union[pl.Expr, str],
) -> pl.Expr
Returns: See Separation Check Output
Example:
When Sparsity is Detected:
- Use regularization (lambda_ > 0)
- Consider zero-inflated models
- Check for sparse predictor-response combinations
Multicollinearity¶
vif¶
Variance Inflation Factor per predictor: VIF_j = 1 / (1 - R²_j).
Returns: See VIF Output
Example:
VIF > 5 (moderate) / > 10 (severe) typically flags problematic collinearity. The intercept is not included.
generalized_vif¶
Generalized VIF (GVIF) for grouped predictors such as one-hot dummies.
ps.generalized_vif(
*x: Union[pl.Expr, str],
group_sizes: list[int], # e.g. [1, 1, 3] for two scalars + a 3-level dummy
) -> pl.Expr
Returns: See GVIF Output
Example:
# x1, x2 are scalars; cat_a/cat_b/cat_c are dummies for one categorical
df.select(
ps.generalized_vif("x1", "x2", "cat_a", "cat_b", "cat_c",
group_sizes=[1, 1, 3]).alias("gvif")
)
For single-column groups GVIF coincides with vif.
high_vif_predictors¶
Boolean mask of features whose VIF exceeds threshold (default 10).
Returns: See VIF Mask Output
Example:
OLS Residual Battery¶
Each function fits OLS internally and returns one residual per row.
standardized_residuals¶
r_i / sqrt(MSE).
ps.standardized_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
Example:
studentized_residuals¶
Internally studentized: r_i / sqrt(MSE * (1 - h_ii)).
ps.studentized_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
externally_studentized_residuals¶
Leave-one-out studentized residuals; t-distributed with n - p - 1 df under the null.
ps.externally_studentized_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
residual_outliers¶
Boolean outlier mask based on |studentized residual| > threshold (default 2.0).
ps.residual_outliers(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
threshold: float = 2.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Outlier Mask Output
Example:
Influence / Leverage¶
leverage¶
Hat-matrix diagonal h_ii. Sums to p; flags h_ii > 2p/n are conventional.
Returns: See Leverage Output
Example:
cooks_distance¶
D_i = (e_i² / (p · MSE)) · (h_ii / (1 - h_ii)²). Common cutoffs: D_i > 4/n or D_i > 1.
ps.cooks_distance(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
with_intercept: bool = True,
) -> pl.Expr
Returns: See Cook's Distance Output
Example:
dffits¶
Scaled change in fitted value when observation i is dropped. Cutoff: |DFFITS_i| > 2 * sqrt(p / n).
ps.dffits(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
with_intercept: bool = True,
) -> pl.Expr
Returns: See DFFITS Output
influential_cooks¶
Boolean mask of observations with Cook's distance above threshold (default 4 / n).
ps.influential_cooks(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
threshold: float | None = None,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Influence Mask Output
influential_dffits¶
Boolean mask of observations with |DFFITS| above threshold (default 2 * sqrt(p / n)).
ps.influential_dffits(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
threshold: float | None = None,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Influence Mask Output
high_leverage_points¶
Boolean mask of high-leverage observations. Takes only feature columns; default threshold 2 * p / n.
ps.high_leverage_points(
*x: Union[pl.Expr, str],
threshold: float | None = None,
add_intercept: bool = True,
) -> pl.Expr
Returns: See Influence Mask Output
GLM Residuals¶
Each function fits the appropriate GLM internally and returns one residual per row. All accept lambda_=0.0 for ridge-penalized IRLS.
logistic_pearson_residuals¶
ps.logistic_pearson_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
logistic_deviance_residuals¶
ps.logistic_deviance_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
logistic_working_residuals¶
IRLS adjusted-dependent-variable residuals.
ps.logistic_working_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
poisson_pearson_residuals¶
ps.poisson_pearson_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
poisson_deviance_residuals¶
ps.poisson_deviance_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
poisson_working_residuals¶
ps.poisson_working_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
Goodness of Fit¶
pearson_chi_squared_logistic¶
Σ pearson_residual² from a logistic fit, plus residual degrees of freedom. For a well-specified model X² / df_resid ≈ 1.
ps.pearson_chi_squared_logistic(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Chi-Squared Output
pearson_chi_squared_poisson¶
Σ pearson_residual² from a Poisson fit, plus residual degrees of freedom.
ps.pearson_chi_squared_poisson(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Chi-Squared Output
Gamma GLM Diagnostics¶
Residual and dispersion diagnostics for Gamma GLM fits. Each function fits a Gamma GLM
internally (log link, lambda_=0.0 by default) and returns one value per row or a
scalar summary.
gamma_dispersion_deviance¶
Deviance-based dispersion estimate: D / (n - p). For a well-specified Gamma model this
should be close to 1.
ps.gamma_dispersion_deviance(
y: Union[pl.Expr, str], # Positive response
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: Struct{dispersion: Float64, deviance: Float64, df_residual: Float64}
Example:
gamma_dispersion_pearson¶
Pearson-statistic-based dispersion estimate: X² / (n - p).
ps.gamma_dispersion_pearson(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: Struct{dispersion: Float64, pearson_chi_squared: Float64, df_residual: Float64}
gamma_pearson_chi_squared¶
Σ pearson_residual² from a Gamma GLM fit, plus residual degrees of freedom.
ps.gamma_pearson_chi_squared(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Chi-Squared Output
gamma_standardized_pearson_residuals¶
Standardized Pearson residuals for Gamma GLM: (y - μ̂) / (μ̂ · sqrt(φ̂)).
ps.gamma_standardized_pearson_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
Example:
gamma_standardized_deviance_residuals¶
Standardized deviance residuals for Gamma GLM: sign(y - μ̂) · sqrt(d_i / φ̂).
ps.gamma_standardized_deviance_residuals(
y: Union[pl.Expr, str],
*x: Union[pl.Expr, str],
lambda_: float = 0.0,
with_intercept: bool = True,
) -> pl.Expr
Returns: See Residual Diagnostics Output
Example:
Diagnostic Workflow¶
import polars as pl
import polars_statistics as ps
# 1. Check multicollinearity before fitting
cond = df.select(ps.condition_number("x1", "x2", "x3"))
vif = df.select(ps.vif("x1", "x2", "x3"))
# 2. For binary outcomes, check separation
sep = df.select(ps.check_binary_separation("y", "x1", "x2"))
if sep["separation"][0]["has_separation"]:
model = df.select(ps.logistic("y", "x1", "x2", lambda_=1.0))
else:
model = df.select(ps.logistic("y", "x1", "x2"))
# 3. After fitting OLS, scan for outliers and influential points
df.select(ps.residual_outliers("y", "x1", "x2").alias("outliers"))
df.select(ps.influential_cooks("y", "x1", "x2").alias("influence"))
# 4. Or get the full per-observation diagnostic battery in one call (OLS or any GLM)
df.select(ps.augment("y", "x1", "x2", model="ols").alias("aug")).unnest("aug")
Note: The list-shaped OLS diagnostics above (
leverage,cooks_distance,dffits,studentized_residuals,externally_studentized_residuals) andaugment's equivalent row-aligned fields agree elementwise for the same data — they are two different shapes (List[Float64]per call vs. oneStructrow per observation) over the same underlying computation, not two different implementations.
See Also¶
- GLM Models - GLMs with regularization
- Linear Models - Linear regression variants