clophfit.fitting.residual_tests =============================== .. py:module:: clophfit.fitting.residual_tests .. autoapi-nested-parse:: Formal residual diagnostics: the four assumptions a least-squares fit makes. Each assumption has a picture that shows it failing and a test that measures it: ====================== =============================== =============================== Assumption What failure looks like Test ====================== =============================== =============================== Homoscedasticity funnel / fan against prediction Breusch-Pagan (Koenker), White Independence waves along the titration Durbin-Watson, lag-1 r, runs Normality curved QQ, skewed histogram Shapiro-Wilk, KS, Anderson-Darling No outliers isolated extreme points externally studentized t, PRESS ====================== =============================== =============================== Two things make these tests easy to misread on titration plates, and every function here reports accordingly: * **They are pooled over hundreds of points**, so any real effect, however small, reaches p ~ 0. Each test therefore also returns an effect size - R^2, the DW statistic, W, D, a rate or a ratio - and those, not the p-values, are what compare models. * **The residuals belong to a fit.** A plate spends several parameters per well, so residuals shrink and pick up induced correlation even when the noise is independent and calibrated. Durbin-Watson is not expected at exactly 2, and the Kolmogorov-Smirnov test against N(0, 1) mixes shape with that shrinkage (see ``residuals.qq_fit``); the Lilliefors form, with location and scale estimated, tests shape alone. The outlier and PRESS measures need each point's leverage, computed per well from the weighted Jacobian of :func:`~clophfit.fitting.models.binding_1site` at the supplied parameters. For a Bayesian multi-well fit those are posterior means and the latent per-well pH offset is not counted, so leverage is an approximation there; for a classical per-well fit it is exact. Functions --------- .. autoapisummary:: clophfit.fitting.residual_tests.breusch_pagan clophfit.fitting.residual_tests.white_test clophfit.fitting.residual_tests.durbin_watson clophfit.fitting.residual_tests.normality_tests clophfit.fitting.residual_tests.durbin_watson_null clophfit.fitting.residual_tests.runs_test clophfit.fitting.residual_tests.runs_test_null clophfit.fitting.residual_tests.well_leverage clophfit.fitting.residual_tests.studentized_outliers clophfit.fitting.residual_tests.press_statistics clophfit.fitting.residual_tests.residual_tests Module Contents --------------- .. py:function:: breusch_pagan(z, yhat, x) Test whether the squared residual follows the prediction or x (Breusch-Pagan). The studentized form, ``n R^2`` of ``z^2`` regressed on the regressors, is used rather than the original, which assumes normal errors and rejects on kurtosis alone. :param z: Standardised residuals. :type z: np.ndarray :param yhat: Predictions. :type yhat: np.ndarray :param x: Titration coordinate (pH or concentration). :type x: np.ndarray :returns: ``lm``, ``df`` (2), ``p`` and ``r2`` - the fraction of the squared residuals' variance the fan explains, the effect size to compare. :rtype: dict[str, float] .. rubric:: Examples >>> import numpy as np >>> rng = np.random.default_rng(0) >>> y = rng.uniform(100, 1000, 800) >>> fan = breusch_pagan(rng.normal(0, y / 500), y, rng.uniform(5, 9, 800)) >>> fan["p"] < 1e-6 True .. py:function:: white_test(z, yhat, x) White's test: squared residual on ลท, x, their squares and cross product. Catches a variance that bends with the signal - high at both ends, say - which the linear Breusch-Pagan design cannot see. :param z: Standardised residuals. :type z: np.ndarray :param yhat: Predictions. :type yhat: np.ndarray :param x: Titration coordinate. :type x: np.ndarray :returns: ``lm``, ``df`` (5), ``p`` and ``r2``. :rtype: dict[str, float] .. py:function:: durbin_watson(table, *, z_col = 'std_res') Durbin-Watson pooled within series: each well's label ordered along the titration. ``DW = sum((e_t - e_{t-1})^2) / sum(e_t^2)`` with the differences taken only inside a (well, label) series, so a jump between wells is never counted. Well below its null, neighbouring steps share an error (a wave, or a misplaced x); above it, they alternate. The null is **not 2** for series this short: independent *errors* give ``2 (n - 1) / n`` per series - 1.71 for a seven-step titration - which ``dw_expected`` reports. Fitted *residuals* are not independent even then: the fit makes neighbours alternate, and for a binding curve that lifts the null well above 2. Use :func:`durbin_watson_null` for the null of the fitted residuals. :param table: Residual table with ``well``, ``label``, ``step`` (or ``x``) and *z_col*. :type table: pd.DataFrame :param z_col: The residual column. :type z_col: str :returns: ``dw``, ``dw_expected`` (its value for independent errors of these series lengths), the pooled lag-1 correlation ``lag1_r``, ``n_pairs`` and ``p``, a two-sided normal-approximation p-value for ``lag1_r`` against 0. :rtype: dict[str, float] .. py:function:: normality_tests(z, *, n_mc = 999, seed = 0) Shapiro-Wilk, Kolmogorov-Smirnov against N(0, 1), Lilliefors and Anderson-Darling. * Shapiro-Wilk ``W`` measures shape only; it is the most powerful of the three, so on hundreds of points it rejects small departures - read W. * KS against N(0, 1) tests shape *and* scale: a calibrated fit still fails it when its residuals are shrunk by the parameters it spent. * Lilliefors is KS with location and scale estimated, p by Monte Carlo (``scipy.stats.goodness_of_fit``): shape only, like Shapiro-Wilk, but weighted towards the centre rather than the tails. * Anderson-Darling, also with location and scale estimated and p by Monte Carlo: shape only, weighted towards the tails, where a Student-t or mixture likelihood is meant to differ from the Normal. :param z: Standardised residuals. :type z: np.ndarray :param n_mc: Monte Carlo samples for the Lilliefors and Anderson-Darling p-values. :type n_mc: int :param seed: Seed for those samples, so a report is reproducible. :type seed: int :returns: ``sw_w``, ``sw_p``, ``ks_n01_d``, ``ks_n01_p``, ``lillie_d``, ``lillie_p``, ``ad_a2``, ``ad_p``. :rtype: dict[str, float] .. py:function:: durbin_watson_null(table, params, *, is_ph = True) Durbin-Watson and lag-1 correlation expected of *fitted* residuals, per label. With independent, calibrated errors the weighted residuals of a least-squares fit have covariance ``I - H``, ``H`` the hat matrix of the well's weighted Jacobian (both labels, one shared K). So, per series, ``E[sum (e_t - e_{t-1})^2] = tr(D'D (I - H))`` and ``E[sum e_t^2] = tr(I - H)`` with ``D`` the first-difference operator along the steps; pooling those traces over wells gives the null DW of the pooled statistic, and the summed lag-1 covariances over the summed variances of the leading and trailing points the null lag-1 correlation. Plateau points carry the most leverage, so the series ends are the least variable, which is why the two differ. A multi-well Bayesian fit spends more than these parameters (the latent pH axis), so its true null lies further from the independent-error value. :param table: Residual table with ``well``, ``label``, ``step`` (or ``x``), ``x``, ``raw_res`` and ``sigma``. :type table: pd.DataFrame :param params: Well to fitted ``K``/``S0_