Quick Start Guide

This guide will help you get up and running with Elote quickly. We’ll cover the basics of creating competitors, recording match results, and using arenas for tournaments.

Not sure which rating system fits your data? See How to choose a rating system for a decision guide and a runnable comparison harness.

Basic Usage

Let’s start with a simple example using the Elo rating system:

from elote import EloCompetitor

# Create two competitors with different initial ratings
player1 = EloCompetitor(initial_rating=1200)
player2 = EloCompetitor(initial_rating=1300)

# Check the probability of player2 beating player1
win_probability = player2.expected_score(player1)
print(f"Player 2 win probability: {win_probability:.2%}")
# Output: Player 2 win probability: 64.01%

# Record a match result where player1 beats player2 (an upset!)
player1.beat(player2)
# OR equivalently:
# player2.lost_to(player1)

# Check the updated ratings
print(f"Player 1 new rating: {player1.rating}")
print(f"Player 2 new rating: {player2.rating}")

# Check the new win probability
new_probability = player2.expected_score(player1)
print(f"Player 2 new win probability: {new_probability:.2%}")
# The probability will have decreased

Recording Draws

Elote also supports recording draws between competitors:

from elote import EloCompetitor

player1 = EloCompetitor(initial_rating=1200)
player2 = EloCompetitor(initial_rating=1300)

# Record a draw
player1.tied(player2)
# OR equivalently:
# player2.tied(player1)

# Check the updated ratings
print(f"Player 1 new rating: {player1.rating}")
print(f"Player 2 new rating: {player2.rating}")

Recording Scores

beat, lost_to and tied all take an optional scores keyword: the two competitors’ scores in the argument order of the call, whichever of them won.

from elote import MasseyCompetitor

team_a, team_b = MasseyCompetitor(), MasseyCompetitor()

team_a.beat(team_b, scores=(35, 3))
# The same game from the loser's side -- the pair is not reordered:
# team_b.lost_to(team_a, scores=(3, 35))

Through an arena, pass the pair in (a, b) order along with an explicit outcome:

from elote import LambdaArena, MasseyCompetitor

arena = LambdaArena(lambda a, b: True, base_competitor=MasseyCompetitor)
arena.matchup("Team A", "Team B", outcome=1.0, scores=(35, 3))
arena.matchup("Team A", "Team C", outcome=0.0, scores=(7, 24))   # C won; order unchanged

Scores must be non-negative, finite, and consistent with the result being recorded – anything else raises ValueError before any rating or history changes. Rating systems that model margin of victory (currently Massey) consume the payload; the rest validate and ignore it. Omitting scores leaves every system on its existing unit-score behaviour, so the argument is fully backward compatible.

Scores from a dataset

Dataset rows carry their scores in the attributes mapping, so the training and benchmarking helpers take a score_keys pair naming the two keys, in (a_score_key, b_score_key) order. CollegeFootballDataset writes home_score and away_score:

from elote import CollegeFootballDataset, benchmark_competitors, MasseyCompetitor, KeenerCompetitor

split = CollegeFootballDataset(start_year=2020, end_year=2021).time_split(test_ratio=0.2)

results = benchmark_competitors(
    [{"class": MasseyCompetitor, "name": "Massey"}, {"class": KeenerCompetitor, "name": "Keener"}],
    split,
    lambda a, b, attributes=None: True,
    score_keys=("home_score", "away_score"),
)

score_keys is also accepted by train_arena_with_dataset, train_and_evaluate_arena and evaluate_competitor. It defaults to None, which trains on unit margins exactly as before. Rows whose attributes are missing, lack either key, or carry a non-numeric value are trained without scores rather than raising, since real feeds have gaps; a score pair that is present but inconsistent with the row’s outcome is still a ValueError. Evaluation only reads expected scores, so score_keys has no effect on the held-out half of a split.

Using Different Rating Systems

Elote supports multiple rating systems. Here’s how to use Glicko instead of Elo:

from elote import GlickoCompetitor

# Create two competitors with Glicko ratings
player1 = GlickoCompetitor(initial_rating=1500, initial_rd=350)
player2 = GlickoCompetitor(initial_rating=1600, initial_rd=300)

# Check win probability
win_probability = player2.expected_score(player1)
print(f"Player 2 win probability: {win_probability:.2%}")

# Record a match result
player1.beat(player2)

# Check the updated ratings and rating deviations
print(f"Player 1: rating={player1.rating}, rd={player1.rd}")
print(f"Player 2: rating={player2.rating}, rd={player2.rd}")

Running Tournaments with Arenas

For managing multiple competitors and matches, use an Arena:

from elote import LambdaArena
import random
import json

# Define a comparison function (returns True if a beats b)
def compare_numbers(a, b):
    return a > b

# Generate 1000 random matchups between numbers 1-10
matchups = [(random.randint(1, 10), random.randint(1, 10)) for _ in range(1000)]

# Create arena and run tournament with appropriate initial ratings
arena = LambdaArena(
    compare_numbers,
    base_competitor_kwargs={"initial_rating": 1200}
)
arena.tournament(matchups)

# Display final rankings
rankings = arena.leaderboard()
print(json.dumps(rankings, indent=2))

The output will show the numbers ranked by their ratings, with higher numbers having higher ratings.

Custom Arenas

You can create custom arenas for specific use cases:

from elote import Arena
from elote import EloCompetitor

class ChessArena(Arena):
    def __init__(self):
        self.competitors = {}

    def register_competitor(self, name, rating=1200):
        self.competitors[name] = EloCompetitor(initial_rating=rating)

    def record_result(self, name1, name2, winner=None):
        if name1 not in self.competitors:
            self.register_competitor(name1)
        if name2 not in self.competitors:
            self.register_competitor(name2)

        if winner == name1:
            self.competitors[name1].beat(self.competitors[name2])
        elif winner == name2:
            self.competitors[name2].beat(self.competitors[name1])
        else:
            self.competitors[name1].tied(self.competitors[name2])

    def leaderboard(self):
        return sorted(
            [{"name": name, "rating": competitor.rating}
             for name, competitor in self.competitors.items()],
            key=lambda x: x["rating"],
            reverse=True
        )

# Usage
chess_arena = ChessArena()

# Register some players with initial ratings
chess_arena.register_competitor("Magnus", 2800)
chess_arena.register_competitor("Hikaru", 2750)

# Record match results
chess_arena.record_result("Magnus", "Hikaru", winner="Magnus")
chess_arena.record_result("Fabiano", "Magnus", winner="Magnus")  # Auto-registers Fabiano

# Get rankings
rankings = chess_arena.leaderboard()
for rank, player in enumerate(rankings, 1):
    print(f"{rank}. {player['name']}: {player['rating']}")

Ensemble Rating System

For more robust ratings, you can use the Ensemble rating system that combines multiple rating methods:

from elote import EnsembleCompetitor
from elote import EloCompetitor, GlickoCompetitor

# Create an ensemble with Elo and Glicko components
player1 = EnsembleCompetitor(
    rating_systems=[
        (EloCompetitor(initial_rating=1200), 0.7),
        (GlickoCompetitor(initial_rating=1500, initial_rd=350), 0.3)
    ]
)

player2 = EnsembleCompetitor(
    rating_systems=[
        (EloCompetitor(initial_rating=1300), 0.7),
        (GlickoCompetitor(initial_rating=1600, initial_rd=350), 0.3)
    ]
)

# Use just like any other competitor
win_probability = player2.expected_score(player1)
print(f"Player 2 win probability: {win_probability:.2%}")

player1.beat(player2)

# All underlying ratings are automatically updated
new_probability = player2.expected_score(player1)
print(f"Player 2 new win probability: {new_probability:.2%}")

Next Steps

Now that you understand the basics of Elote, you can:

  • Explore the different rating systems in more detail

  • Learn about advanced arena configurations

  • Check out the examples directory for real-world use cases

  • Integrate Elote into your own projects

For more detailed information, see the following sections: