Skip to content

Commit

Permalink
Use f64 in Bradley-Terry
Browse files Browse the repository at this point in the history
  • Loading branch information
dustalov committed Jul 6, 2024
1 parent a45f816 commit 80ffb4b
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 33 deletions.
2 changes: 1 addition & 1 deletion python/evalica/evalica.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def py_counting(m: npt.NDArray[np.int64]) -> npt.NDArray[np.int64]: ...


def py_bradley_terry(
m: npt.NDArray[np.int64], tolerance: float, limit: int,
m: npt.NDArray[np.float64], tolerance: float, limit: int,
) -> tuple[npt.NDArray[np.float64], int]: ...


Expand Down
2 changes: 1 addition & 1 deletion python/evalica/naive.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import numpy.typing as npt


def bradley_terry(M: npt.NDArray[np.int64], tolerance: float = 1e-8) -> tuple[ # noqa: N803
def bradley_terry(M: npt.NDArray[np.float64], tolerance: float = 1e-8) -> tuple[ # noqa: N803
npt.NDArray[np.float64], int]:
T = M.T + M # noqa: N806
active = T > 0
Expand Down
61 changes: 54 additions & 7 deletions python/evalica/test_evalica.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from pathlib import Path

import hypothesis.strategies as st
import numpy as np
import numpy.typing as npt
import pandas as pd
import pytest
from hypothesis import given
from hypothesis.extra.numpy import arrays

import evalica
from evalica import Winner


def test_version() -> None:
Expand Down Expand Up @@ -47,8 +51,8 @@ def test_counting(m: npt.NDArray[np.int64]) -> None:
assert np.isfinite(p).all()


@given(arrays(dtype=np.int64, shape=(5, 5), elements=st.integers(0, 256)))
def test_bradley_terry(m: npt.NDArray[np.int64]) -> None:
@given(arrays(dtype=np.float64, shape=(5, 5), elements=st.integers(0, 256)))
def test_bradley_terry(m: npt.NDArray[np.float64]) -> None:
p, iterations = evalica.bradley_terry(m, 1e-4, 100)

assert m.shape[0] == len(p)
Expand Down Expand Up @@ -95,25 +99,25 @@ def test_eigen(m: npt.NDArray[np.int64]) -> None:


@pytest.fixture()
def simple() -> npt.NDArray[np.int64]:
def simple() -> npt.NDArray[np.float64]:
return np.array([
[0, 1, 2, 0, 1],
[2, 0, 2, 1, 0],
[1, 2, 0, 0, 1],
[1, 2, 1, 0, 2],
[2, 0, 1, 3, 0],
], dtype=np.int64)
], dtype=np.float64)


@pytest.fixture()
def simple_win_tie(simple: npt.NDArray[np.int64]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
def simple_win_tie(simple: npt.NDArray[np.float64]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
T = np.minimum(simple, simple.T).astype(np.float64) # noqa: N806
W = simple - T # noqa: N806

return W, T


def test_bradley_terry_simple(simple: npt.NDArray[np.int64], tolerance: float = 1e-4) -> None:
def test_bradley_terry_simple(simple: npt.NDArray[np.float64], tolerance: float = 1e-4) -> None:
p_naive, _ = evalica.bradley_terry_naive(simple, tolerance)
p, _ = evalica.bradley_terry(simple, tolerance, 100)

Expand All @@ -127,5 +131,48 @@ def test_newman_simple(simple_win_tie: tuple[npt.NDArray[np.float64], npt.NDArra
p_naive, _, _ = evalica.newman_naive(w, t, .5, tolerance)
p, _, _ = evalica.newman(w, t, .5, tolerance, 100)

# TODO: they are diverging
assert p == pytest.approx(p_naive, abs=tolerance)


@pytest.fixture()
def food() -> tuple[list[int], list[int], list[evalica.Winner]]:
df_food = pd.read_csv(Path().parent.parent / "food.csv", dtype=str)

xs = df_food["left"]
ys = df_food["right"]
ws = df_food["winner"].map({
"left": Winner.X,
"right": Winner.Y,
"tie": Winner.Draw,
})

# TODO: we need a better DX than this
index: dict[str, int] = {}

for xy in zip(xs, ys, strict=False):
for e in xy:
index[e] = index.get(e, len(index))

return [index[x] for x in xs], [index[y] for y in ys], ws.tolist()


def test_bradley_terry_food(food: tuple[list[int], list[int], list[evalica.Winner]]) -> None:
xs, ys, ws = food

_wins, _ties = evalica.matrices(xs, ys, ws)
wins = _wins.astype(np.float64) + _ties / 2

scores, iterations = evalica.bradley_terry(wins, 1e-4, 100)

assert len(set(xs) | set(ys)) == len(scores)
assert np.isfinite(scores).all()
assert iterations > 0


def test_elo_food(food: tuple[list[int], list[int], list[evalica.Winner]]) -> None:
xs, ys, ws = food

scores = evalica.elo(xs, ys, ws, 1500, 30, 400)

assert len(scores) == len(set(xs) | set(ys))
assert np.isfinite(scores).all()
32 changes: 11 additions & 21 deletions src/bradley_terry.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use ndarray::{Array1, Array2, ArrayView2, Axis};

pub fn bradley_terry(m: &ArrayView2<i64>, tolerance: f64, limit: usize) -> (Array1<f64>, usize) {
pub fn bradley_terry(m: &ArrayView2<f64>, tolerance: f64, limit: usize) -> (Array1<f64>, usize) {
assert_eq!(m.shape()[0], m.shape()[1], "The matrix must be square");

let totals = m.t().to_owned() + m;

let active = totals.mapv(|x| x > 0);
let active = totals.mapv(|x| x > 0.0);

let w: Array1<i64> = m.sum_axis(Axis(1));
let w: Array1<f64> = m.sum_axis(Axis(1));

let mut z: Array2<f64> = Array2::zeros(m.raw_dim());

Expand All @@ -32,7 +32,7 @@ pub fn bradley_terry(m: &ArrayView2<i64>, tolerance: f64, limit: usize) -> (Arra
let d = z.column(i).sum();

if d != 0.0 {
scores_new[i] = w[i] as f64 / d;
scores_new[i] = w[i] / d;
}
}

Expand Down Expand Up @@ -116,13 +116,13 @@ mod tests {

use super::{bradley_terry, newman};

fn matrix() -> Array2<i64> {
fn matrix() -> Array2<f64> {
return array![
[0, 1, 2, 0, 1],
[2, 0, 2, 1, 0],
[1, 2, 0, 0, 1],
[1, 2, 1, 0, 2],
[2, 0, 1, 3, 0]
[0.0, 1.0, 2.0, 0.0, 1.0],
[2.0, 0.0, 2.0, 1.0, 0.0],
[1.0, 2.0, 0.0, 0.0, 1.0],
[1.0, 2.0, 1.0, 0.0, 2.0],
[2.0, 0.0, 1.0, 3.0, 0.0]
];
}

Expand All @@ -144,19 +144,9 @@ mod tests {
}
}

fn matrix2() -> Array2<i64> {
return array![
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
];
}

#[test]
fn test_newman() {
let m = matrix2();
let m = matrix();
let tolerance = 1e-8;
let limit = 100;

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ fn py_counting<'py>(py: Python, m: PyReadonlyArray2<'py, i64>) -> PyResult<Py<Py
#[pyfunction]
fn py_bradley_terry<'py>(
py: Python,
m: PyReadonlyArray2<'py, i64>,
m: PyReadonlyArray2<'py, f64>,
tolerance: f64,
limit: usize,
) -> PyResult<(Py<PyArray1<f64>>, usize)> {
Expand Down
4 changes: 2 additions & 2 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ use ndarray::{Array2, ArrayView1, ArrayView2};

use crate::Winner;

pub fn compute_ties_and_wins(m: &ArrayView2<i64>) -> (Array2<i64>, Array2<i64>) {
pub fn compute_ties_and_wins(m: &ArrayView2<f64>) -> (Array2<f64>, Array2<f64>) {
let mut t = m.to_owned();

for ((i, j), t) in t.indexed_iter_mut() {
*t = std::cmp::min(m[[i, j]], m[[j, i]]);
*t = f64::min(m[[i, j]], m[[j, i]]);
}

let w = m - &t;
Expand Down

0 comments on commit 80ffb4b

Please sign in to comment.