Skip to content

Commit 518e973

Browse files
committed
add a simple script for plotting public vs secret run times
1 parent e07a86d commit 518e973

4 files changed

Lines changed: 827 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ dependencies = [
2323
"fastapi[all]",
2424
"uvicorn",
2525
"jinja2",
26+
"matplotlib>=3.10.8",
27+
"numpy>=2.2.6",
2628
]
2729

2830
[project.optional-dependencies]

src/kernelbot/cogs/admin_cog.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import io
12
import json
23
import subprocess
34
import tempfile
@@ -102,6 +103,10 @@ def __init__(self, bot: "ClusterBot"):
102103
name="show-stats", description="Show stats for the kernelbot"
103104
)(self.show_bot_stats)
104105

106+
self.show_timing_correlation = bot.admin_group.command(
107+
name="show-timing-correlation", description="Show correlation between public and private run timings"
108+
)(self.show_timing_correlation)
109+
105110
self.resync = bot.admin_group.command(
106111
name="resync", description="Trigger re-synchronization of slash commands"
107112
)(self.resync)
@@ -793,6 +798,67 @@ async def show_bot_stats(self, interaction: discord.Interaction, last_day_only:
793798
msg += "\n```"
794799
await send_discord_message(interaction, msg, ephemeral=True)
795800

801+
@with_error_handling
802+
@discord.app_commands.describe(leaderboard="Which leaderboard")
803+
@discord.app_commands.autocomplete(leaderboard=leaderboard_name_autocomplete)
804+
async def show_timing_correlation(self, interaction: discord.Interaction, leaderboard: str):
805+
is_admin = await self.admin_check(interaction)
806+
if not is_admin:
807+
await send_discord_message(
808+
interaction,
809+
"You need to have Admin permissions to run this command",
810+
ephemeral=True,
811+
)
812+
return
813+
814+
with self.bot.leaderboard_db as db:
815+
stats = db.generate_correlation_stats(leaderboard)
816+
817+
if len(stats) == 0:
818+
await send_discord_message(
819+
interaction,
820+
f"No ranked runs (with matching public/secret scores) found for leaderboard {leaderboard}.")
821+
return
822+
823+
import matplotlib.pyplot as plt
824+
import numpy as np
825+
826+
files = []
827+
for runner, pairs in stats.items():
828+
public_scores, secret_scores = zip(*pairs)
829+
830+
plt.figure(figsize=(8, 6))
831+
plt.scatter(public_scores, secret_scores, alpha=0.6)
832+
833+
# Diagonal reference line
834+
min_val = min(min(public_scores), min(secret_scores))
835+
max_val = max(max(public_scores), max(secret_scores))
836+
plt.plot([min_val, max_val], [min_val, max_val], 'r--', alpha=0.5, label='y=x')
837+
838+
# Calculate correlation
839+
corr = np.corrcoef(public_scores, secret_scores)[0, 1]
840+
841+
plt.xlabel('Public Score')
842+
plt.ylabel('Secret Score')
843+
plt.title(f'{leaderboard} on {runner} - Correlation: {corr:.3f}')
844+
plt.legend()
845+
plt.grid(True, alpha=0.3)
846+
847+
plt.tight_layout()
848+
849+
buf = io.BytesIO()
850+
plt.savefig(buf, format='png', dpi=150)
851+
buf.seek(0)
852+
853+
files.append(discord.File(buf, filename=f'correlation_{runner}.png'))
854+
plt.close()
855+
856+
await send_discord_message(
857+
interaction,
858+
"Correlation of running times for public and private runs",
859+
files=files,
860+
ephemeral=True)
861+
796862
@with_error_handling
797863
async def resync(self, interaction: discord.Interaction):
798864
"""Admin command to resync slash commands"""

src/libkernelbot/leaderboard_db.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dataclasses
22
import datetime
33
import json
4+
from collections import defaultdict
45
from typing import Dict, List, Optional
56

67
import psycopg2
@@ -812,6 +813,50 @@ def _generate_stats(self, last_day: bool = False):
812813

813814
return result
814815

816+
def generate_correlation_stats(self, leaderboard_name: str) -> dict[str, list]:
817+
query = """
818+
SELECT
819+
s.id,
820+
r.score,
821+
r.runner,
822+
r.secret
823+
FROM leaderboard.runs r
824+
JOIN leaderboard.submission s ON r.submission_id = s.id
825+
JOIN leaderboard.leaderboard l ON s.leaderboard_id = l.id
826+
WHERE
827+
l.name = %s
828+
AND r.score IS NOT NULL
829+
AND r.passed
830+
"""
831+
832+
self.cursor.execute(query, (leaderboard_name,))
833+
834+
matches = defaultdict(list)
835+
for run in self.cursor.fetchall():
836+
id, score, runner, secret = run
837+
matches[(id, runner)].append((float(score), secret))
838+
839+
results = defaultdict(list)
840+
for entry, values in matches.items():
841+
if len(values) != 2:
842+
continue
843+
secret = None
844+
public = None
845+
if values[0][1]:
846+
secret = values[0][0]
847+
else:
848+
public = values[0][0]
849+
850+
if values[1][1]:
851+
secret = values[1][0]
852+
else:
853+
public = values[1][0]
854+
855+
if secret is not None and public is not None:
856+
results[entry[1]].append((public, secret))
857+
858+
return results
859+
815860
def get_user_from_id(self, id: str) -> Optional[str]:
816861
try:
817862
self.cursor.execute(

0 commit comments

Comments
 (0)