Source code for compml.frequentist.tests.non_parametrics.quade
"""Quade test."""
from typing import Any
import numpy as np
from scipy.stats import f, rankdata
from compml.evaluation_scores import EvaluationScores
from compml.frequentist.tests.base import BaseFrequentistTest
[docs]
class QuadeTest(BaseFrequentistTest[EvaluationScores]):
"""Quade 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 Quade 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 Quade test statistic.
Parameters
----------
scores : EvaluationScores
Evaluation scores container.
Returns
-------
float
Quade test statistic.
"""
x = scores.compute_means()
n, k = x.shape
ranges = x.max(axis=1) - x.min(axis=1)
rank_q = rankdata(ranges, method="average")
if self.minimize:
ranks = np.apply_along_axis(rankdata, 1, x, method="average")
else:
ranks = np.apply_along_axis(rankdata, 1, x, method="average")
s_mat = rank_q[:, np.newaxis] * (ranks - (k + 1) / 2.0)
a_val = float(np.sum(s_mat**2))
col_sums = s_mat.sum(axis=0)
b_val = float(np.sum(col_sums**2) / n)
if abs(a_val - b_val) < 1e-15:
return 0.0
return (n - 1) * b_val / (a_val - b_val)
[docs]
def compute_p_value(self, scores: EvaluationScores, statistic: float) -> float:
"""Compute p-value from the Quade test statistic.
Parameters
----------
scores : EvaluationScores
Evaluation scores container.
statistic : float
Quade test statistic.
Returns
-------
float
p-value computed from the Quade test statistic.
"""
df1, df2 = self.__degrees_of_freedom(scores)
return float(f.sf(statistic, df1, df2))
[docs]
def compute_c_value(self, scores: EvaluationScores) -> float:
"""Compute critical value for the Quade test statistic.
Parameters
----------
scores : EvaluationScores
Evaluation scores container.
Returns
-------
float
Critical value at ``1 - alpha`` (upper-tail).
"""
df1, df2 = self.__degrees_of_freedom(scores)
return float(f.ppf(1.0 - self.alpha, df1, df2))
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("Quade test requires at least two algorithms")
if scores.n_datasets < 2:
raise ValueError("Quade 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 __degrees_of_freedom(self, scores: EvaluationScores) -> tuple[int, int]:
"""Degrees of freedom for the Quade test.
Parameters
----------
scores : EvaluationScores
Evaluation scores container.
Returns
-------
tuple[int, int]
Degrees of freedom for the Quade test.
"""
k = scores.n_algorithms
n = scores.n_datasets
return k - 1, (k - 1) * (n - 1)
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")