Hodge Decomposition

Hodge decomposition of pairwise comparison data.

Any skew-symmetric matrix of log-odds (or edge flows on the tournament graph) can be orthogonally decomposed into:

Y = grad(s) + curl(C) + harmonic

where:
  • grad(s) is the transitive component: there exists a potential s_i for each node such that Y_ij ~ s_i - s_j. This is the part that can be calibrated via a standard Bradley-Terry model.

  • curl(C) is the cyclic component: it captures rock-paper-scissors structure that is irreducible to any linear ranking.

  • harmonic captures global topological structure (zero for complete tournaments).

The decomposition is computed via least-squares on the combinatorial Laplacian, following Jiang et al. (2011) “Statistical ranking and combinatorial Hodge theory”.

Calibration strategy

  1. Compute the Hodge decomposition of the empirical log-odds.

  2. Use the gradient component s as BT strengths giving transitive calibrated win probabilities: P_trans(i > j) = sigmoid(s_i - s_j).

  3. Report ||curl||^2 / ||Y||^2 as the fraction of variance due to non-transitive structure, the part your calibration ignores.

class winference.hodge.HodgeResult(potential, gradient_flow, curl_flow, harmonic_flow, transitive_variance, cyclic_variance, harmonic_variance)[source]

Bases: NamedTuple

Result of a Hodge decomposition.

Parameters:
potential: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 0

gradient_flow: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 1

curl_flow: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 2

harmonic_flow: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 3

transitive_variance: float

Alias for field number 4

cyclic_variance: float

Alias for field number 5

harmonic_variance: float

Alias for field number 6

class winference.hodge.HodgeDecomposition(models)[source]

Bases: object

Hodge decomposition of a pairwise comparison matrix.

Parameters:

models (list[str]) – Model identifiers.

Examples

>>> hd = HodgeDecomposition(["A", "B", "C", "D"])
>>> result = hd.fit(win_rate_matrix)
>>> print(f"Cyclic fraction: {result.cyclic_variance:.1%}")
>>> # Calibrate using only the transitive part
>>> p_trans = hd.transitive_win_probability("A", "B")
result: HodgeResult | None
fit(W, weights=None)[source]

Decompose a win-rate matrix.

Parameters:
  • W (ndarray[tuple[Any, ...], dtype[double]]) – Win rate matrix of shape (n, n) where W[i,j] = P(i beats j). Should satisfy W[i,j] + W[j,i] ~ 1.

  • weights (ndarray[tuple[Any, ...], dtype[double]] | None, default: None) – Number of comparisons per pair of shape (n, n), used for weighted least squares. Default: uniform weights.

Return type:

HodgeResult

Returns:

HodgeResult containing decomposition components.

transitive_win_probability(model_a, model_b)[source]

P(a beats b) using only the gradient (transitive) component.

This is the win probability that can be calibrated to a scalar ranking. The cyclic component is dropped.

Parameters:
Return type:

float

transitive_win_matrix()[source]

Full NxN transitive win probability matrix.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

transitive_strengths()[source]

Hodge potential (transitive strength) per model.

Return type:

dict[str, float]

curl_magnitude_per_pair()[source]

NxN matrix of curl magnitude: how much each pair deviates from transitivity.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

worst_pairs(k=10)[source]

Top-k pairs with largest cyclic residual.

Parameters:

k (int, default: 10)

Return type:

list[tuple[str, str, float]]

summary()[source]

Quick summary of the decomposition.

Return type:

dict[str, float | int]