There are many ways to rank competitors using results from one-on-one games. Professional leagues such as the NFL and NBA use deterministic rules based on record, division standings, and head-to-head outcomes. Tennis uses a cumulative point system to determine ATP world rankings. College sports, however, typically rely on committees to make subjective decisions.
Committee-based systems generate controversy, yet relying solely on win–loss records is inadequate in some leagues because teams play vastly different schedules.
My view is that the most important property of a ranking is consistency—if competitor
This document introduces a deterministic ranking system that prioritizes consistency above all else, and then enhances it with tunable parameters that address realistic edge cases.
- Prepare your data: Place game results in
data/games.jsonfollowing the Input File Format. - Competitor Filtering: Create
data/filter.jsonwith competitors to include in the final ranking (optional). - Configure parameters: Set environment variables in
.env(optional). - Run the solver: Execute
python rank.pyand response to console prompts. - View results: Find the ranking in
rankings/output.jsonor specified output file.
Suppose we have
A game is an ordered pair
Each competitor is assigned a unique integer rank from
-
$1 \leq r_i \leq n$ for all$i$ , -
$r_i \neq r_j$ for all$i \neq j$ .
In other words, the rank vector
An inconsistency occurs when a lower-ranked competitor defeats a higher-ranked one. For a game
- If the ranking agrees with the outcome (
$r_i < r_j$ ), the inconsistency score is$0$ . - If the ranking contradicts the result (
$r_i > r_j$ ), the inconsistency score is$r_i - r_j$ .
These cases can be written compactly as:
The total inconsistency score is the sum of these values over all games. To prioritize consistency, we minimize this score subject to ranking constraints:
This formulation is valid and captures the idea of minimizing ranking contradictions. However, it has some shortcomings, which motivate additional refinements.
Optimization problem
-
Respecting head-to-head results, even when inconsistency magnitudes tie.
-
Considering strength of schedule, once consistency is maximized.
We address each separately.
Consider three competitors
-
$c_1$ beats$c_2$ -
$c_1$ loses to$c_3$
Suppose
- Ranking
$c_1 = 1, c_2 = 2$ yields a total inconsistency score of$r_3 - 1$ . - Ranking
$c_1 = 2, c_2 = 1$ yields a total inconsistency score of$1 + (r_3 - 2) = r_3 - 1$ .
Thus, both are equally consistent under
To accomplish this, we introduce a parameter
Interpretation of
-
$\alpha = 0$ : pure magnitude—this recovers formulation$(1)$ . -
$\alpha = 1$ : ties in magnitude are broken by counting inconsistent games. -
$\alpha > 1$ : strongly prioritizes minimizing the number of inconsistent games, potentially over the magnitude of them.
Requiring
It is still possible for multiple rankings to have identical inconsistency scores. To distinguish these, we incorporate a strength of schedule (SOS) measure.
Critically, we evaluate strength of schedule only using consistent games. Inconsistent games already strongly affect the objective through inconsistency penalties.
For competitor
-
$W_i$ : multiset of opponents that$c_i$ defeated in consistent games. -
$L_i$ : multiset of opponents that defeated$c_i$ in consistent games.
Define:
- Quality of wins: $$ Q_i^{\text{win}} = \sum_{c_j \in W_i} (n - r_j + 1)^k $$
- Severity of losses: $$ Q_i^{\text{loss}} = \sum_{c_j \in L_i} (r_j)^k $$
Let:
-
$Q_{\text{max}}^{\text{win}} = \max_{m} Q_m^{\text{win}}$ (maximum quality of wins among all competitors) -
$Q_{\text{max}}^{\text{loss}} = \max_{m} Q_m^{\text{loss}}$ (maximum severity of losses among all competitors)
We then define:
Here:
-
$\epsilon > 0$ ensures division remains well-defined and keeps SOS strictly less than$1$ in magnitude, which will preserve consistency dominance. -
$\lambda \in [0, 1]$ sets the win/loss weighting. -
$k \geq 0$ controls the emphasis on opponent quality.
Parameter interpretations:
-
$\lambda = 1$ : only wins influence SOS -
$\lambda = 0$ : only losses influence SOS -
$\lambda = 0.5$ : balanced wins and losses -
$k = 0$ : all wins and losses count equally -
$k = 1$ : linear emphasis on opponent rank -
$k > 1$ : rewards elite wins heavily and penalizes bad losses harshly
SOS behaves intuitively:
- Adding a consistent win always increases
$\text{SOS}_i$ . - Adding a consistent loss always decreases
$\text{SOS}_i$ .
We incorporate strength of schedule adding the following term to the objective:
Multiplying by
- Because the objective is minimized, a higher SOS is preferred when paired with a smaller
$r_i$ (a better rank). - This aligns the minimization with the intuitive principle that stronger schedules should correspond to better ranks.
From equation
-
All
$\text{SOS}_i = \lambda$ and competitors are ordered optimally, giving$\lambda \cdot n(n+1)/2$ -
All
$\text{SOS}_i = \lambda - 1$ and competitors are ordered optimally, giving$(\lambda - 1) \cdot n(n+1)/2$
The scaling factor
Thus, the system remains philosophically consistent: strength of schedule matters only after consistency is maximized.
Combining everything, the final optimization problem is:
Where:
-
$I$ is defined in$(2)$ -
$\text{SOS}_i$ is defined in$(3)$ $W_i = {c_j : (c_i, c_j) \in G \text{ and } r_i < r_j}$ $L_i = {c_j : (c_j, c_i) \in G \text{ and } r_i > r_j}$ -
$\alpha \geq 0$ is an integer $\epsilon > 0, k \geq 0, \lambda \in [0, 1]$
This formulation:
- Minimizes inconsistency magnitude
-
Minimizes number of inconsistencies (via
$\alpha$ ) - Breaks remaining ties using strength of schedule, scaled so that consistency always dominates
Together, these components produce rankings that are consistent, interpretable, and tunably sensitive to quality of competition.
The optimization problem in
Accordingly, the solver employs stochastic discrete optimization consisting of two complementary procedures:
- Simulated Annealing — a global heuristic designed to escape poor local minima;
- Sliding Optimization — a deterministic local refinement method restricted to small contiguous moves.
The annealing stage explores the permutation space broadly and identifies a near-optimal basin, while the sliding stage performs a fine-grained local search to converge to a stable minimum of the objective.
Both methods evaluate candidate rankings using the complete objective function in
Simulated annealing provides a probabilistic framework for minimizing a discrete objective function that may contain many local minima.
Let
At each iteration:
-
Two competitors are selected uniformly at random.
-
Their positions in the ordering are swapped, producing a new permutation
$r'$ . -
The loss difference
$\Delta=L(r')-L(r)$ is computed. -
The new state is accepted according to the Metropolis criterion:
$$ r \leftarrow \begin{cases} r' & \text{if } \Delta < 0,\ r' & \text{with probability } \exp(-\Delta / T),\ r & \text{otherwise}, \end{cases} $$
where
$T>0$ is the current temperature.
The temperature decreases geometrically every 1,000 iterations according to
where
This schedule enables broad exploration early (high
Several properties of this implementation are worth noting:
- Move structure. Each proposal is a single transposition, the simplest non-trivial move on the permutation group. Because any permutation can be expressed as a sequence of transpositions, and the algorithm selects every possible transposition with positive probability, the induced Markov chain is irreducible: every ranking is reachable from every other ranking. This ensures the annealing process can fully explore the permutation space rather than becoming trapped in a restricted subset.
- Energy landscape. Because inconsistency losses are integer-valued and SOS contributions are strictly bounded by construction, the annealing dynamics primarily explore the integer part of the objective with fractional corrections discouraging but never overriding the consistency-driven structure.
- Best-state tracking. The algorithm retains the best permutation observed at any temperature, guaranteeing monotonic improvement of the reported solution even though the Markov chain itself may accept uphill moves.
This stage terminates after a predetermined amount iterations and returns a near-optimal ranking that typically lies within the attraction basin of the true minimum.
Following annealing, a deterministic sliding optimization procedure refines the ranking by examining small localized adjustments.
For each competitor currently at position
where
For each feasible shift, a new permutation is formed by removing the competitor from its original position and reinserting it at the candidate location; all other relative orders are preserved.
For each competitor:
- All upward and downward slides within the window are evaluated.
- The slide yielding the smallest loss is identified.
- If that slide improves the global objective, it is applied immediately.
- The process restarts from the top of the ranking after every successful improvement.
This “first-improvement restart” strategy ensures that local dependencies are respected: repositioning a single competitor often alters the optimal moves for those around it, so restarting prevents stale assumptions about local structure.
The sliding stage repeats until either:
-
a full pass over all competitors yields no improvement, or
-
the predetermined maximum number of passes is reached.
Because slides are strictly loss-decreasing, this stage converges to a local minimum within the neighborhood of contiguous moves up to size
Empirically, annealing identifies a strong basin, and sliding then resolves fine-grained rank ordering that transposition dynamics alone are unlikely to discover.
This section describes the structure of the repository, the required environment variables, the expected file formats, and how to run the ranking procedure on an arbitrary set of pairwise game results. An example using 2025 college football data is also provided.
.
├── rank.py # Optimization solver implementing the ranking algorithm
├── helpers/ # Helper scripts
├── data/ # Input game and competitor filter files
├── rankings/ # Output files
├── .env # Environment variable definitions (optional)
└── README.md # This document
The core solver is rank.py, which loads game results, applies the filtering rules, constructs the optimization problem described in
- Clone or download this repository
- Install dependencies:
pip install -r requirements.txt
- Optional: Create a
.envfile for custom parameters (see Environment Variables)
The solver reads several configuration parameters from the environment. Any parameter omitted from the environment uses its built-in default value.
| Variable | Meaning | Default |
|---|---|---|
ALPHA |
Penalty per inconsistent game (favors fewer inconsistencies over magnitude) | 1 |
K |
Exponent in SOS computations (emphasizes elite wins/bad losses) | 2.0 |
LAMBDA |
Weighting of wins (1.0) vs. losses (0.0) in SOS | 0.5 |
EPSILON |
Small constant ensuring strict SOS bounds | 0.001 |
SEED |
Random seed for reproducible results | 42 |
ANNEALING_ITER |
Total simulated annealing iterations | 200000 |
COOLING_RATE |
Temperature reduction rate (per 1,000 iterations) | 0.99 |
WINDOW_SEARCH_SIZE |
Maximum slide distance in local optimization | 3 |
MAX_SLIDE_PASSES |
Maximum full passes during sliding optimization | 1000 |
The solver expects the game list in a JSON file. Each entry must contain exactly two fields:
[
{ "winner": "Team A", "loser": "Team B" },
{ "winner": "Team C", "loser": "Team D" },
...
]- Each item represents one completed game.
- The objects are interpreted literally: in the example above, Team A defeated Team B, and Team C defeated Team D.
This file may contain any number of games and any number of repeated matchups. The algorithm automatically treats the list as a multiset, as described in the formulation. The file must be placed in the data/ directory.
You may optionally restrict the ranking to a subset of competitors—for example, only FBS teams, or only teams in a particular conference.
To do this, create a file containing a JSON array of competitor names:
["Team A", "Team B", "Team C", ...]The solver computes a ranking for all competitors present in the game data. Then, it filters out any competitors not listed in the filter file. This approach ensures that games against filtered-out opponents (e.g., an FBS team beating an FCS team) are still counted in the inconsistency and Strength of Schedule calculation for the remaining competitors, while excluding those opponents from the final ranking list.
Rankings are written to the rankings/ directory. Output file name is specified as input in the console, but defaults to output.json.
An output file has the structure:
{
"parameters": {
"ALPHA": 1,
"K": 2.5,
"LAMBDA": 0.67,
"EPSILON": 0.001,
"SEED": 42,
"ANNEALING_ITER": 2000000,
"COOLING_RATE": 0.998,
"MAX_SLIDE_PASSES": 2000,
"WINDOW_SEARCH_SIZE": 5
},
"info": {
"final_loss": 1979.991882573818,
"loss_after_annealing": 1981.993108416549,
"slide_improvements_made": 63,
"total_games": 1351,
"total_competitors": 265,
"ranked_competitors": 136
},
"ranking": [
{
"rank": 1,
"competitor": "Team A",
"inconsistency_score": 0,
"SOS": 0.658520019039728,
"inconsistent_games": []
},
...
{
"rank": 5,
"competitor": "Team B",
"inconsistency_score": 53,
"SOS": 0.6627604786107557,
"inconsistent_games": [
{
"type": "loss",
"opponent": "Team C",
"magnitude": 52
}
]
},
...
]
}This repository includes an example using real college football data. It contains:
- a CFB game scraper:
helpers/pull_cfb.py, - a JSON array of FBS teams:
data/fbs_team.json - a final college football ranking:
rankings/cfb_2025_ranking.json
This example demonstrates how to generate fully reproducible rankings from publicly available CollegeFootballData (CFBD) results.
The pull_cfb.py file in the helpers/ directory downloads game data for a given year using the CollegeFootballData API.
- Get a free API key from CollegeFootballData.com
- Add it to your
.envfile:
CFBD_API_KEY=your_key_here
cd helpers
python pull_cfb.pyThe script will:
-
Prompt you for a year (e.g., 2024)
-
Download all completed FBS/FCS games for that year
-
Save to
data/cfb_YYYY_games.json
The generated file follows the required input format and includes:
-
All regular season and postseason games
-
Both FBS and FCS teams
-
Games filtered to completed status only
Even though the goal is to rank only the FBS teams, the scraped data includes both FBS and FCS teams because they can play each other. This ensures that FBS teams are properly rewarded or penalized for beating or losing to an FCS team.
To filter out the FCS teams from the final ranking, the filter file fbs_teams.json in the data/ directory can be specified in the console when running the ranking script.
The computed FBS ranking can be found in rankings/cfb_2025_ranking.json. The file contains information on the parameters used.
The repository contains a few helper scripts to pull and simulate miscellaneous data. Details will not be specified here, but the scripts are fairly straight-forward.
The repository also includes some game data, team data, and rankings. Files in the data/ directory ending in _games contain game data. Files in the data/ directory ending in _teams contain team filters. Files in the rankings/ directory contain the ranking computed from the games and team filter, if applicable.