Skip to content

Regression Diagnostics

Tools for detecting multicollinearity, quasi-separation, influence and outliers, plus residual batteries for OLS and GLM fits.

condition_number

Compute condition number diagnostics to detect multicollinearity in design matrices.

ps.condition_number(
    *x: Union[pl.Expr, str],
    with_intercept: bool = True,
) -> pl.Expr

Returns: See Condition Number Output

Example:

df.select(ps.condition_number("x1", "x2", "x3").alias("diagnostics"))

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:

df.select(ps.check_binary_separation("success", "predictor1", "predictor2"))

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:

df.select(ps.check_count_sparsity("count", "x1", "x2"))

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).

ps.vif(*x: Union[pl.Expr, str]) -> pl.Expr

Returns: See VIF Output

Example:

df.select(ps.vif("x1", "x2", "x3").alias("vif"))

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).

ps.high_vif_predictors(
    *x: Union[pl.Expr, str],
    threshold: float = 10.0,
) -> pl.Expr

Returns: See VIF Mask Output

Example:

df.select(ps.high_vif_predictors("x1", "x2", "x3", threshold=5.0))


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:

df.select(ps.standardized_residuals("y", "x1", "x2").alias("r_std"))


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:

df.select(ps.residual_outliers("y", "x1", "x2", threshold=2.5))


Influence / Leverage

leverage

Hat-matrix diagonal h_ii. Sums to p; flags h_ii > 2p/n are conventional.

ps.leverage(
    *x: Union[pl.Expr, str],
    with_intercept: bool = True,
) -> pl.Expr

Returns: See Leverage Output

Example:

df.select(ps.leverage("x1", "x2").alias("h"))


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:

df.select(ps.cooks_distance("y", "x1", "x2").alias("d"))


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


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"))

See Also