Ranks rows by how far they sit from the centre of the data ONCE THE CORRELATIONS ARE ACCOUNTED FOR, which is what a per-column check cannot do: a row can be unremarkable on every variable separately and still be impossible jointly – a person 1.5 m tall weighing 140 kg is inside both marginal ranges and outside the cloud.
Arguments
- data
A data frame or numeric matrix; non-numeric columns are dropped. Rows with any missing value are skipped, and reported as
NA.- alpha
Significance level for the
outlierflag (default 0.001, deliberately strict: at 0.05 one row in twenty is flagged by construction).- robust
Use the median/MAD centre and scale (default
TRUE) .
Value
A data frame of class bricklayer_outliers with
row, distance (the square root of the squared Mahalanobis
distance), p_value and outlier, ordered by descending
distance.
Details
Under multivariate normality the squared distance is chi-square on
p degrees of freedom, which gives the p-value and the cut-off.
robust = TRUE centres on the coordinate-wise median and scales by
a MAD-based covariance instead of the mean and sample covariance. This
matters more than it sounds: outliers inflate the very covariance used
to judge them, so with several of them the classical distance hides
exactly the rows it is meant to find (the masking effect).
Exactly collinear columns are refused rather than repaired: the distance
is undefined there, and flooring the covariance's eigenvalues to make it
invertible would return numbers governed by the floor rather than by the
data. Drop the redundant column first –
top_correlations() will identify it.
References
Mahalanobis PC (1936). On the generalised distance in statistics. Proceedings of the National Institute of Sciences of India 2(1), 49–55.
See also
core_tukey_fences() for the per-column
version, rule_within_n_mads() to turn
it into a validation rule.
Examples
set.seed(1)
n <- 200
df <- data.frame(height = stats::rnorm(n, 170, 10))
df$weight <- df$height * 0.5 + stats::rnorm(n, 0, 5)
# A row that is ordinary on each variable but impossible jointly.
df[1, ] <- list(height = 150, weight = 140)
out <- mahalanobis_outliers(df)
head(out, 3)
#> ── Mahalanobis outliers (robust) ─────────────────────────────────
#> ! 1 of 3 rows beyond alpha = 0.001
#>
#> row distance p_value outlier
#> 1 9.45 <2e-16 TRUE
#> 61 2.76 0.0224 FALSE
#> 14 2.70 0.0258 FALSE
#> ──────────────────────────────────────────────────────────────────
# It is flagged, even though neither value is a marginal outlier.
out$row[1] == 1
#> [1] TRUE
range(df$height)
#> [1] 147.8530 194.0162
range(df$weight)
#> [1] 67.07546 140.00000
# The classical version can be fooled by the outliers it should find.
mahalanobis_outliers(df, robust = FALSE)$distance[1] <
mahalanobis_outliers(df, robust = TRUE)$distance[1]
#> [1] FALSE
# A duplicated column has no distance defined, and is refused.
dup <- df
dup$height2 <- dup$height
try(mahalanobis_outliers(dup))
#> Error : the columns are collinear, so no Mahalanobis distance is defined. Drop the redundant column(s) -- drop_constant() and top_correlations() will find them.