Skip to content

Pseudoinverse makes Huber and adaptive updates treat nullspace shocks as zero surprise #93

Description

@microprediction

Summary

At current default-branch commit 4b26c0c3e38e7fc7f868981c61a576880f970e86, the Mahalanobis control paths in HuberCovariance and AdaptiveEwaCovariance call try_invert, which returns a Moore–Penrose pseudoinverse for a singular covariance.

A pseudoinverse assigns zero quadratic cost to residuals in the covariance nullspace. Consequently, a shock in an exactly constant/unseen asset is classified as zero distance:

  • HuberCovariance gives it full weight, so its influence becomes unbounded precisely at zero prior variance.
  • AdaptiveEwaCovariance classifies it as less surprising than an ordinary observation and keeps the baseline forgetting rate rather than accelerating.

This boundary is common in equity work (halted/stale securities, IPOs/new columns, short p >= n histories, exact factor constraints) and is discontinuous: an arbitrarily small positive variance produces the intended strong downweighting/detection, while exactly zero variance reverses the decision.

This is distinct from #85 (Tyler's numerically indefinite rank-one collapse) and #91 (GMV optimization with singular covariance). Here the input is a valid PSD covariance and the failure is in using pseudoinverse geometry for outlier/regime-control decisions.

Relevant implementation and contracts

Deterministic public-API reproduction

The 20 burn-in rows are zero-mean, orthogonal, and have variances diag(1, sigma**2). The next observation is a shock of 1e6 in the second asset.

import numpy as np
from precise import HuberCovariance, AdaptiveEwaCovariance

a = np.tile([1., 1., -1., -1.], 5)
b = np.tile([1., -1., 1., -1.], 5)
M = 1e6

for sigma in [0.0, 1e-6]:
    X = np.column_stack([a, sigma * b])
    est = HuberCovariance(r=.05, c=2.5).fit(X)
    print("Huber before", sigma, np.diag(est.covariance_))
    est.partial_fit([0., M])
    print("Huber after ", sigma, est.location_, np.diag(est.covariance_))

for sigma in [0.0, 1e-6]:
    X = np.column_stack([a, sigma * b])
    est = AdaptiveEwaCovariance(r=.05, max_r=.5).fit(X)
    est.partial_fit([0., M])
    print("Adaptive", sigma, est._state["sbar"], np.diag(est.covariance_))

Observed:

Huber before 0.0   [1. 0.]
Huber after  0.0   [~0, 5.00000000e+04] [9.5e-01, 5.0e+10]

Huber before 1e-6  [1.e+00, 1.e-12]
Huber after  1e-6  [~0, 1.76776695e-07] [9.500e-01, 1.575e-12]

Adaptive 0.0       sbar=0.9    diag=[9.5e-01, 5.0e+10]
Adaptive 1e-6      sbar=5e22   diag=[5.0e-01, 5.0e+11]

For Huber, the exact-zero and 1e-12-variance results differ by about 3.2e22 in the affected covariance entry.

Why this violates bounded influence

For the two-dimensional diagonal case, let the prior variance in the shock direction be epsilon > 0, shock size M, default c=2.5, and r=.05. Then:

d2 = M**2 / epsilon
w**2 = c**2 * p * epsilon / M**2

The post-update variance is therefore bounded independently of M:

(1-r)*epsilon + r*w**2*M**2
= (.95 + .05*12.5)*epsilon
= 1.575*epsilon

At exactly epsilon=0, the pseudoinverse deletes the shock direction, so d2=0, w=1, and the update is r*M**2, which grows without bound. This is not a small numerical discrepancy; the robust estimator's defining influence property flips at the singular boundary.

For the adaptive estimator, the same deletion changes a genuinely new-direction shock from enormous surprise to ratio=0: sbar moves from 1 to 0.9 and r_eff stays at the baseline.

Equity impact

An exactly constant coordinate occurs naturally when:

  • a security is halted or has stale/missing-forward-filled prices;
  • a new constituent has no return history;
  • the cross-section is wider than the empirical burn-in (p >= n);
  • residualization or duplicate instruments create exact linear constraints.

The first valid move after such a period is precisely the observation that should be treated cautiously. Instead, Huber can absorb its square at full weight and contaminate the covariance and mean; the adaptive estimator can fail to recognize the new direction as a regime change. After that first update the covariance may become nonsingular, but the unbounded state mutation has already occurred.

Suggested direction

Use a separate inverse policy for Mahalanobis control decisions rather than changing the public precision_/pseudoinverse semantics globally. Defensible approaches include:

  1. a scale-relative SPD eigenvalue floor/ridge before inversion; or
  2. explicit nullspace handling, treating any nonzero residual component in a zero-variance direction as maximally surprising.

Any ridge/floor should be relative to the covariance scale so the result remains unit-equivariant. The computed quadratic form should also be checked as finite and nonnegative before it controls a state update.

Suggested tests:

  1. The reproduction above: Huber influence remains bounded for an exactly constant coordinate.
  2. Continuity or explicitly documented regularized behavior as epsilon approaches zero.
  3. p > n_burn data, where singularity is unavoidable.
  4. Adaptive forgetting treats a new nullspace direction as surprise, not calm.
  5. Global data rescaling leaves weights/rate decisions unchanged.

Uncertainty

Mahalanobis distance is mathematically undefined in a direction with exactly zero modeled variance, so the finite policy at that boundary is a design choice. The verified defect is narrower: assigning distance zero is incompatible with the advertised Huber bounded-influence behavior and adaptive surprise semantics, and it creates an unbounded/discontinuous update on supported inputs.

I searched current open issues and pull requests for Huber, adaptive, Mahalanobis, pseudoinverse, singular covariance, and nullspace behavior; no existing report covers this defect.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions