Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
F
formula student elo
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Iterations
Wiki
Requirements
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Package registry
Container registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
GET racing FOSS
formula student elo
Commits
dc658c30
Commit
dc658c30
authored
1 month ago
by
Marius Heidenreich
Browse files
Options
Downloads
Patches
Plain Diff
claude first version
parents
Branches
Branches containing commit
No related tags found
No related merge requests found
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
main.py
+572
-0
572 additions, 0 deletions
main.py
with
572 additions
and
0 deletions
main.py
0 → 100644
+
572
−
0
View file @
dc658c30
import
os
import
csv
import
math
import
glob
import
pandas
as
pd
import
matplotlib.pyplot
as
plt
from
pathlib
import
Path
import
numpy
as
np
from
datetime
import
datetime
# Constants for Elo calculation
K_FACTOR
=
32
# Standard chess K-factor
INITIAL_ELO
=
1500
# Starting Elo for all teams
class
EloSystem
:
def
__init__
(
self
,
csv_directory
,
output_directory
):
self
.
csv_directory
=
csv_directory
self
.
output_directory
=
output_directory
self
.
teams
=
{}
# Dictionary to store team Elo ratings
self
.
match_history
=
[]
# List to store all match results for history tracking
self
.
initialize_output_directory
()
def
initialize_output_directory
(
self
):
"""
Create output directory if it doesn
'
t exist
"""
os
.
makedirs
(
self
.
output_directory
,
exist_ok
=
True
)
os
.
makedirs
(
os
.
path
.
join
(
self
.
output_directory
,
'
images
'
),
exist_ok
=
True
)
def
load_and_process_csv_files
(
self
):
"""
Load all CSV files and process them to calculate Elo ratings
"""
csv_files
=
glob
.
glob
(
os
.
path
.
join
(
self
.
csv_directory
,
'
*.csv
'
))
if
not
csv_files
:
print
(
f
"
No CSV files found in
{
self
.
csv_directory
}
"
)
return
print
(
f
"
Found
{
len
(
csv_files
)
}
CSV files to process
"
)
# Process each CSV file (representing a competition)
for
csv_file
in
sorted
(
csv_files
):
print
(
f
"
Processing
{
os
.
path
.
basename
(
csv_file
)
}
...
"
)
competition_name
=
os
.
path
.
splitext
(
os
.
path
.
basename
(
csv_file
))[
0
]
teams_in_competition
=
[]
# Read the CSV file
with
open
(
csv_file
,
'
r
'
,
newline
=
''
,
encoding
=
'
utf-8
'
)
as
file
:
reader
=
csv
.
reader
(
file
)
# Skip header if exists (assuming first row might be headers)
try
:
first_row
=
next
(
reader
)
# If the first row doesn't start with a number, assume it's a header
if
not
first_row
[
0
].
strip
().
isdigit
():
rank
=
1
else
:
# It's not a header, it's actual data
rank
=
int
(
first_row
[
0
].
strip
())
team_name
=
first_row
[
1
].
strip
()
if
len
(
first_row
)
>
1
else
first_row
[
0
].
strip
()
# Initialize the team if not seen before
if
team_name
not
in
self
.
teams
:
self
.
teams
[
team_name
]
=
{
'
elo
'
:
INITIAL_ELO
,
'
competitions
'
:
0
,
'
history
'
:
[]
}
teams_in_competition
.
append
(
team_name
)
except
StopIteration
:
# File is empty
continue
# Process remaining rows
for
row
in
reader
:
if
not
row
:
# Skip empty rows
continue
# Check if row starts with a number (ranking)
if
row
[
0
].
strip
().
isdigit
():
rank
=
int
(
row
[
0
].
strip
())
team_name
=
row
[
1
].
strip
()
if
len
(
row
)
>
1
else
row
[
0
].
strip
()
else
:
# If no explicit ranking, use row index
team_name
=
row
[
0
].
strip
()
# Initialize the team if not seen before
if
team_name
not
in
self
.
teams
:
self
.
teams
[
team_name
]
=
{
'
elo
'
:
INITIAL_ELO
,
'
competitions
'
:
0
,
'
history
'
:
[]
}
teams_in_competition
.
append
(
team_name
)
# Update competition count for each team
for
team_name
in
teams_in_competition
:
self
.
teams
[
team_name
][
'
competitions
'
]
+=
1
# Update Elo ratings based on this competition's results
self
.
update_elo_for_competition
(
teams_in_competition
,
competition_name
)
def
update_elo_for_competition
(
self
,
teams_in_order
,
competition_name
):
"""
Update Elo ratings for all teams in a competition
"""
# For each team, compare with teams ranked below them
date
=
datetime
.
now
().
strftime
(
"
%Y-%m-%d
"
)
# Use current date as competition date
for
i
in
range
(
len
(
teams_in_order
)):
for
j
in
range
(
i
+
1
,
len
(
teams_in_order
)):
team_a
=
teams_in_order
[
i
]
team_b
=
teams_in_order
[
j
]
# Team A is ranked higher than Team B
self
.
update_elo_pair
(
team_a
,
team_b
,
1
,
0
,
competition_name
,
date
)
def
update_elo_pair
(
self
,
team_a
,
team_b
,
score_a
,
score_b
,
competition_name
,
date
):
"""
Update Elo ratings for a pair of teams based on their match outcome
"""
# Get current Elo ratings
rating_a
=
self
.
teams
[
team_a
][
'
elo
'
]
rating_b
=
self
.
teams
[
team_b
][
'
elo
'
]
# Calculate expected scores
expected_a
=
self
.
expected_score
(
rating_a
,
rating_b
)
expected_b
=
self
.
expected_score
(
rating_b
,
rating_a
)
# Update Elo ratings
new_rating_a
=
rating_a
+
K_FACTOR
*
(
score_a
-
expected_a
)
new_rating_b
=
rating_b
+
K_FACTOR
*
(
score_b
-
expected_b
)
# Store match in history
match_info
=
{
'
date
'
:
date
,
'
competition
'
:
competition_name
,
'
team_a
'
:
team_a
,
'
team_b
'
:
team_b
,
'
old_elo_a
'
:
rating_a
,
'
old_elo_b
'
:
rating_b
,
'
new_elo_a
'
:
new_rating_a
,
'
new_elo_b
'
:
new_rating_b
,
'
score_a
'
:
score_a
,
'
score_b
'
:
score_b
}
self
.
match_history
.
append
(
match_info
)
# Update team Elo ratings and history
self
.
teams
[
team_a
][
'
elo
'
]
=
new_rating_a
self
.
teams
[
team_b
][
'
elo
'
]
=
new_rating_b
self
.
teams
[
team_a
][
'
history
'
].
append
({
'
date
'
:
date
,
'
elo
'
:
new_rating_a
,
'
competition
'
:
competition_name
})
self
.
teams
[
team_b
][
'
history
'
].
append
({
'
date
'
:
date
,
'
elo
'
:
new_rating_b
,
'
competition
'
:
competition_name
})
def
expected_score
(
self
,
rating_a
,
rating_b
):
"""
Calculate the expected score for a player with rating_a against a player with rating_b
"""
return
1
/
(
1
+
math
.
pow
(
10
,
(
rating_b
-
rating_a
)
/
400
))
def
generate_website
(
self
):
"""
Generate static HTML website with Elo ratings and visualizations
"""
# Create main index page
self
.
create_index_page
()
# Create team-specific pages
for
team_name
in
self
.
teams
:
self
.
create_team_page
(
team_name
)
# Create visualizations
self
.
create_visualizations
()
print
(
f
"
Website generated in
{
self
.
output_directory
}
"
)
def
create_index_page
(
self
):
"""
Create the main index.html page
"""
# Sort teams by Elo rating
sorted_teams
=
sorted
(
self
.
teams
.
items
(),
key
=
lambda
x
:
x
[
1
][
'
elo
'
],
reverse
=
True
)
html_content
=
f
"""
<!DOCTYPE html>
<html lang=
"
en
"
>
<head>
<meta charset=
"
UTF-8
"
>
<meta name=
"
viewport
"
content=
"
width=device-width, initial-scale=1.0
"
>
<title>Team Elo Rankings</title>
<style>
body {{
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}}
h1, h2 {{
color: #333;
}}
table {{
border-collapse: collapse;
width: 100%;
margin-top: 20px;
}}
th, td {{
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}}
th {{
background-color: #f2f2f2;
font-weight: bold;
}}
tr:nth-child(even) {{
background-color: #f9f9f9;
}}
tr:hover {{
background-color: #f5f5f5;
}}
.chart-container {{
margin-top: 30px;
display: flex;
justify-content: center;
}}
.chart-container img {{
max-width: 100%;
height: auto;
}}
a {{
color: #0066cc;
text-decoration: none;
}}
a:hover {{
text-decoration: underline;
}}
</style>
</head>
<body>
<h1>Team Elo Rankings</h1>
<p>Rankings based on
{
len
(
self
.
match_history
)
}
matches from
{
self
.
count_competitions
()
}
competitions.</p>
<div class=
"
chart-container
"
>
<img src=
"
images/top_teams_elo.png
"
alt=
"
Top Teams Elo Chart
"
>
</div>
<h2>Current Rankings</h2>
<table>
<tr>
<th>Rank</th>
<th>Team</th>
<th>Elo Rating</th>
<th>Competitions</th>
</tr>
"""
# Add rows for each team
for
rank
,
(
team_name
,
team_data
)
in
enumerate
(
sorted_teams
,
1
):
html_content
+=
f
"""
<tr>
<td>
{
rank
}
</td>
<td><a href=
"
team_
{
self
.
clean_filename
(
team_name
)
}
.html
"
>
{
team_name
}
</a></td>
<td>
{
int
(
team_data
[
'
elo
'
])
}
</td>
<td>
{
team_data
[
'
competitions
'
]
}
</td>
</tr>
"""
html_content
+=
"""
</table>
<div class=
"
chart-container
"
>
<img src=
"
images/elo_distribution.png
"
alt=
"
Elo Distribution
"
>
</div>
<p><small>Generated on {}</small></p>
</body>
</html>
"""
.
format
(
datetime
.
now
().
strftime
(
"
%Y-%m-%d %H:%M:%S
"
))
# Write the HTML file
with
open
(
os
.
path
.
join
(
self
.
output_directory
,
'
index.html
'
),
'
w
'
,
encoding
=
'
utf-8
'
)
as
f
:
f
.
write
(
html_content
)
def
create_team_page
(
self
,
team_name
):
"""
Create a dedicated page for a specific team
"""
team_data
=
self
.
teams
[
team_name
]
# Generate Elo history chart for this team
if
team_data
[
'
history
'
]:
self
.
create_team_elo_history_chart
(
team_name
)
# Get matches involving this team
team_matches
=
[
match
for
match
in
self
.
match_history
if
match
[
'
team_a
'
]
==
team_name
or
match
[
'
team_b
'
]
==
team_name
]
html_content
=
f
"""
<!DOCTYPE html>
<html lang=
"
en
"
>
<head>
<meta charset=
"
UTF-8
"
>
<meta name=
"
viewport
"
content=
"
width=device-width, initial-scale=1.0
"
>
<title>
{
team_name
}
- Elo Profile</title>
<style>
body {{
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}}
h1, h2 {{
color: #333;
}}
table {{
border-collapse: collapse;
width: 100%;
margin-top: 20px;
}}
th, td {{
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}}
th {{
background-color: #f2f2f2;
font-weight: bold;
}}
tr:nth-child(even) {{
background-color: #f9f9f9;
}}
tr:hover {{
background-color: #f5f5f5;
}}
.chart-container {{
margin-top: 30px;
display: flex;
justify-content: center;
}}
.chart-container img {{
max-width: 100%;
height: auto;
}}
a {{
color: #0066cc;
text-decoration: none;
}}
a:hover {{
text-decoration: underline;
}}
.summary-stats {{
display: flex;
justify-content: space-around;
margin: 20px 0;
flex-wrap: wrap;
}}
.stat-box {{
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
margin: 10px;
flex: 1;
min-width: 150px;
text-align: center;
background-color: #f9f9f9;
}}
.stat-value {{
font-size: 24px;
font-weight: bold;
color: #333;
}}
.stat-label {{
font-size: 14px;
color: #666;
}}
</style>
</head>
<body>
<h1>
{
team_name
}
- Elo Profile</h1>
<p><a href=
"
index.html
"
>← Back to Rankings</a></p>
<div class=
"
summary-stats
"
>
<div class=
"
stat-box
"
>
<div class=
"
stat-value
"
>
{
int
(
team_data
[
'
elo
'
])
}
</div>
<div class=
"
stat-label
"
>Current Elo</div>
</div>
<div class=
"
stat-box
"
>
<div class=
"
stat-value
"
>
{
team_data
[
'
competitions
'
]
}
</div>
<div class=
"
stat-label
"
>Competitions</div>
</div>
"""
# Calculate win rate if applicable
if
team_matches
:
wins
=
sum
(
1
for
match
in
team_matches
if
(
match
[
'
team_a
'
]
==
team_name
and
match
[
'
score_a
'
]
>
match
[
'
score_b
'
])
or
(
match
[
'
team_b
'
]
==
team_name
and
match
[
'
score_b
'
]
>
match
[
'
score_a
'
]))
win_rate
=
(
wins
/
len
(
team_matches
))
*
100
html_content
+=
f
"""
<div class=
"
stat-box
"
>
<div class=
"
stat-value
"
>
{
len
(
team_matches
)
}
</div>
<div class=
"
stat-label
"
>Total Matches</div>
</div>
<div class=
"
stat-box
"
>
<div class=
"
stat-value
"
>
{
win_rate
:
.
1
f
}
%</div>
<div class=
"
stat-label
"
>Win Rate</div>
</div>
"""
html_content
+=
"""
</div>
<div class=
"
chart-container
"
>
<img src=
"
images/{}_history.png
"
alt=
"
{} Elo History
"
>
</div>
<h2>Match History</h2>
<table>
<tr>
<th>Date</th>
<th>Competition</th>
<th>Opponent</th>
<th>Result</th>
<th>Elo Change</th>
</tr>
"""
.
format
(
self
.
clean_filename
(
team_name
),
team_name
)
# Add rows for each match
for
match
in
sorted
(
team_matches
,
key
=
lambda
x
:
x
[
'
date
'
],
reverse
=
True
):
if
match
[
'
team_a
'
]
==
team_name
:
opponent
=
match
[
'
team_b
'
]
old_elo
=
match
[
'
old_elo_a
'
]
new_elo
=
match
[
'
new_elo_a
'
]
result
=
"
Win
"
if
match
[
'
score_a
'
]
>
match
[
'
score_b
'
]
else
"
Loss
"
else
:
opponent
=
match
[
'
team_a
'
]
old_elo
=
match
[
'
old_elo_b
'
]
new_elo
=
match
[
'
new_elo_b
'
]
result
=
"
Win
"
if
match
[
'
score_b
'
]
>
match
[
'
score_a
'
]
else
"
Loss
"
elo_change
=
new_elo
-
old_elo
change_color
=
"
green
"
if
elo_change
>
0
else
"
red
"
html_content
+=
f
"""
<tr>
<td>
{
match
[
'
date
'
]
}
</td>
<td>
{
match
[
'
competition
'
]
}
</td>
<td><a href=
"
team_
{
self
.
clean_filename
(
opponent
)
}
.html
"
>
{
opponent
}
</a></td>
<td>
{
result
}
</td>
<td style=
"
color:
{
change_color
}
"
>
{
'
+
'
if
elo_change
>
0
else
''
}{
elo_change
:
.
1
f
}
</td>
</tr>
"""
html_content
+=
"""
</table>
<p><small>Generated on {}</small></p>
</body>
</html>
"""
.
format
(
datetime
.
now
().
strftime
(
"
%Y-%m-%d %H:%M:%S
"
))
# Write the HTML file
with
open
(
os
.
path
.
join
(
self
.
output_directory
,
f
'
team_
{
self
.
clean_filename
(
team_name
)
}
.html
'
),
'
w
'
,
encoding
=
'
utf-8
'
)
as
f
:
f
.
write
(
html_content
)
def
create_visualizations
(
self
):
"""
Create various visualizations for the website
"""
# Top teams Elo chart
self
.
create_top_teams_chart
()
# Elo distribution histogram
self
.
create_elo_distribution_chart
()
def
create_top_teams_chart
(
self
):
"""
Create a bar chart of the top teams by Elo rating
"""
plt
.
figure
(
figsize
=
(
12
,
8
))
# Get top 15 teams by Elo
top_teams
=
sorted
(
self
.
teams
.
items
(),
key
=
lambda
x
:
x
[
1
][
'
elo
'
],
reverse
=
True
)[:
15
]
team_names
=
[
team
[
0
]
for
team
in
top_teams
]
elo_ratings
=
[
team
[
1
][
'
elo
'
]
for
team
in
top_teams
]
# Create bar chart
bars
=
plt
.
barh
(
team_names
,
elo_ratings
,
color
=
'
skyblue
'
)
# Add Elo values at the end of each bar
for
i
,
bar
in
enumerate
(
bars
):
plt
.
text
(
bar
.
get_width
()
+
5
,
bar
.
get_y
()
+
bar
.
get_height
()
/
2
,
f
"
{
int
(
elo_ratings
[
i
])
}
"
,
va
=
'
center
'
)
plt
.
xlabel
(
'
Elo Rating
'
)
plt
.
title
(
'
Top Teams by Elo Rating
'
)
plt
.
tight_layout
()
# Save the chart
plt
.
savefig
(
os
.
path
.
join
(
self
.
output_directory
,
'
images
'
,
'
top_teams_elo.png
'
),
dpi
=
100
)
plt
.
close
()
def
create_elo_distribution_chart
(
self
):
"""
Create a histogram of Elo rating distribution
"""
plt
.
figure
(
figsize
=
(
10
,
6
))
# Get all Elo ratings
elo_ratings
=
[
team_data
[
'
elo
'
]
for
team_data
in
self
.
teams
.
values
()]
# Create histogram
plt
.
hist
(
elo_ratings
,
bins
=
20
,
color
=
'
skyblue
'
,
edgecolor
=
'
black
'
)
plt
.
xlabel
(
'
Elo Rating
'
)
plt
.
ylabel
(
'
Number of Teams
'
)
plt
.
title
(
'
Distribution of Team Elo Ratings
'
)
plt
.
grid
(
axis
=
'
y
'
,
alpha
=
0.75
)
plt
.
tight_layout
()
# Save the chart
plt
.
savefig
(
os
.
path
.
join
(
self
.
output_directory
,
'
images
'
,
'
elo_distribution.png
'
),
dpi
=
100
)
plt
.
close
()
def
create_team_elo_history_chart
(
self
,
team_name
):
"""
Create a line chart showing a team
'
s Elo history over time
"""
plt
.
figure
(
figsize
=
(
10
,
6
))
team_history
=
self
.
teams
[
team_name
][
'
history
'
]
# Sort history by date
team_history
.
sort
(
key
=
lambda
x
:
x
[
'
date
'
])
dates
=
[
entry
[
'
date
'
]
for
entry
in
team_history
]
elo_values
=
[
entry
[
'
elo
'
]
for
entry
in
team_history
]
# Create line chart
plt
.
plot
(
range
(
len
(
dates
)),
elo_values
,
marker
=
'
o
'
,
linestyle
=
'
-
'
,
color
=
'
blue
'
)
# Add competition labels
for
i
,
entry
in
enumerate
(
team_history
):
plt
.
annotate
(
entry
[
'
competition
'
],
(
i
,
entry
[
'
elo
'
]),
textcoords
=
"
offset points
"
,
xytext
=
(
0
,
10
),
ha
=
'
center
'
,
fontsize
=
8
,
rotation
=
45
)
plt
.
xticks
(
range
(
len
(
dates
)),
dates
,
rotation
=
45
)
plt
.
ylabel
(
'
Elo Rating
'
)
plt
.
title
(
f
'
{
team_name
}
- Elo Rating History
'
)
plt
.
grid
(
True
,
alpha
=
0.3
)
plt
.
tight_layout
()
# Save the chart
plt
.
savefig
(
os
.
path
.
join
(
self
.
output_directory
,
'
images
'
,
f
'
{
self
.
clean_filename
(
team_name
)
}
_history.png
'
),
dpi
=
100
)
plt
.
close
()
def
count_competitions
(
self
):
"""
Count the number of unique competitions
"""
return
len
(
set
(
match
[
'
competition
'
]
for
match
in
self
.
match_history
))
def
clean_filename
(
self
,
filename
):
"""
Clean a string to make it suitable for use as a filename
"""
# Replace spaces and special characters
return
''
.
join
(
c
if
c
.
isalnum
()
else
'
_
'
for
c
in
filename
).
lower
()
def
main
():
"""
Main function to run the Elo ranking system
"""
import
argparse
parser
=
argparse
.
ArgumentParser
(
description
=
'
Process competition results and generate Elo rankings website
'
)
parser
.
add_argument
(
'
--input
'
,
default
=
'
data
'
,
help
=
'
Directory containing CSV result files
'
)
parser
.
add_argument
(
'
--output
'
,
default
=
'
elo_website
'
,
help
=
'
Output directory for the website
'
)
args
=
parser
.
parse_args
()
# Create and run the Elo system
elo_system
=
EloSystem
(
args
.
input
,
args
.
output
)
elo_system
.
load_and_process_csv_files
()
elo_system
.
generate_website
()
if
__name__
==
"
__main__
"
:
main
()
\ No newline at end of file
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment