Source code for compml.frequentist.tests.non_parametrics.friedman_aligned_ranks

"""Friedman aligned ranks test."""

from typing import Any

import numpy as np
from scipy.stats import chi2, rankdata

from compml.evaluation_scores import EvaluationScores
from compml.frequentist.tests.base import BaseFrequentistTest


[docs] class FriedmanAlignedRanksTest(BaseFrequentistTest[EvaluationScores]): """Friedman aligned ranks test for repeated measurements. The null hypothesis is that the mean scores are equal across all algorithms. """ def __init__(self, alpha: float = 0.05, minimize: bool = False): """Initialize the Friedman aligned ranks test. Parameters ---------- alpha : float Significance level. minimize : bool Whether to minimize the metric (lower scores are better). """ super().__init__(alpha=alpha) self.__validate_arguments(minimize) self.minimize = minimize
[docs] def compute_statistic(self, scores: EvaluationScores) -> float: """Compute the Friedman aligned ranks test statistic. Parameters ---------- scores : EvaluationScores Evaluation scores container. Returns ------- float Friedman aligned ranks test statistic. """ x = scores.compute_means() n, k = x.shape aligned = x - x.mean(axis=1, keepdims=True) flat = aligned.ravel() if self.minimize: r = rankdata(flat, method="average") else: r = rankdata(-flat, method="average") ranks = r.reshape(n, k) sum_sq_col_sums = float(np.sum(ranks.sum(axis=0) ** 2)) sum_sq_row_sums = float(np.sum(ranks.sum(axis=1) ** 2)) nk = n * k numer = (k - 1) * (sum_sq_col_sums - (k * (n**2) / 4.0) * (nk + 1) ** 2) denom = (nk * (nk + 1) * (2 * nk + 1)) / 6.0 - (sum_sq_row_sums / k) if abs(denom) < 1e-15: return 0.0 return numer / denom
[docs] def compute_p_value(self, scores: EvaluationScores, statistic: float) -> float: """Compute p-value from the Friedman aligned ranks test statistic. Parameters ---------- scores : EvaluationScores Evaluation scores container. statistic : float Friedman aligned ranks test statistic. Returns ------- float p-value computed from the Friedman aligned ranks test statistic. """ df = scores.n_algorithms - 1 return float(chi2.sf(statistic, df))
[docs] def compute_c_value(self, scores: EvaluationScores) -> float: """Compute critical value for the Friedman aligned ranks test statistic. Parameters ---------- scores : EvaluationScores Evaluation scores container. Returns ------- float Critical value at ``1 - alpha`` (upper-tail). """ df = scores.n_algorithms - 1 return float(chi2.ppf(1.0 - self.alpha, df))
def _validate_scores(self, scores: EvaluationScores): """Validate that the input scores meet the test assumptions. Parameters ---------- scores : EvaluationScores Scores to validate. Raises ------ ValueError If score structure is not compatible with this test. """ if scores.n_algorithms < 2: raise ValueError( "Friedman aligned ranks test requires at least two algorithms" ) if scores.n_datasets < 2: raise ValueError( "Friedman aligned ranks test requires at least two datasets" ) def _get_metadata(self) -> dict[str, Any]: """Return procedure metadata. Returns ------- dict Metadata including ranking convention. """ return {"minimize": self.minimize} def __validate_arguments(self, minimize: bool): """Validate initialization arguments. Parameters ---------- minimize : bool Whether to minimize the metric (lower scores are better). """ if not isinstance(minimize, bool): raise ValueError("minimize must be a boolean")