This analysis extends the Project 1 chess tournament dataset by computing each player’s expected score using the standard Elo formula, then identifying who most over- and underperformed relative to those expectations.
Formula used (Elo, 1978; Wikipedia — Elo rating system):
\[E_A = \frac{1}{1 + 10^{(R_B - R_A)/400}}\]
where \(R_A\) is the player’s pre-tournament rating and \(R_B\) is each opponent’s pre-tournament rating. The expected score is the sum of \(E_A\) across all 7 rounds (byes excluded). The difference = Actual Score − Expected Score.
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
##
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
##
## group_rows
# ── Pre-ratings lookup ────────────────────────────────────────────────────────
pre_ratings <- c(
`1`=1794, `2`=1553, `3`=1384, `4`=1716, `5`=1655, `6`=1686, `7`=1649,
`8`=1641, `9`=1411, `10`=1365, `11`=1552, `12`=1579, `13`=1552, `14`=1500,
`15`=1425, `16`=1604, `17`=1552, `18`=1449, `19`=1552, `20`=1426,
`21`=1601, `22`=1481, `23`=1436, `24`=1556, `25`=1493, `26`=1520,
`27`=1525, `28`=1563, `29`=1480, `30`=1531, `31`=1199, `32`=1186,
`33`=1363, `34`=1175, `35`=1321, `36`=1388, `37`=1391, `38`=1220,
`39`=1580, `40`=1416, `41`=1387, `42`=1270, `43`=1392, `44`=1356,
`45`=1184, `46`=1403, `47`=1229, `48`=1413, `49`=1222, `50`=1745,
`51`=1419, `52`=1365, `53`=1279, `54`=1388, `55`=1142, `56`=1400,
`57`=1227, `58`=1436, `59`=1082, `60`=1399, `61`=1149, `62`=1107,
`63`=1329, `64`=980, `65`=1153, `66`=1186
)
# ── Tournament results ─────────────────────────────────────────────────────────
# Each row: player_id, name, state, actual_pts, opponents (0 = bye/unplayed)
# Opponents listed in round order 1–7
tournament <- tribble(
~id, ~name, ~state, ~actual_pts, ~opp1, ~opp2, ~opp3, ~opp4, ~opp5, ~opp6, ~opp7,
1, "Gary Hua", "ON", 6.0, 39, 21, 18, 14, 7, 12, 4,
2, "Dakshesh Daruri", "MI", 6.0, 63, 58, 4, 17, 16, 20, 7,
3, "Aditya Bajaj", "MI", 6.0, 8, 61, 25, 21, 11, 13, 12,
4, "Patrick H Schilling", "MI", 5.5, 23, 28, 2, 26, 5, 19, 1,
5, "Hanshi Zuo", "MI", 5.5, 45, 37, 12, 13, 4, 14, 17,
6, "Hansen Song", "OH", 5.0, 34, 29, 11, 35, 10, 27, 21,
7, "Gary Dee Swain", "MI", 5.0, 57, 46, 13, 11, 1, 19, 2,
8, "Bruce Harris", "MI", 5.0, 3, 57, 16, 18, 22, 30, 23,
9, "Frederic Fournier", "ON", 5.0, 14, 48, 51, 43, 31, 33, 35,
10, "Anvit Rao", "MI", 5.0, 62, 52, 31, 27, 6, 46, 29,
11, "Dylan Wang", "MI", 5.0, 38, 31, 6, 7, 3, 27, 25,
12, "Sawyer Smith", "MI", 4.5, 63, 40, 5, 58, 45, 1, 3,
13, "Gary Nirmalan", "MI", 4.5, 60, 54, 7, 5, 23, 3, 43,
14, "Simón Kazuko", "MI", 4.5, 9, 32, 55, 1, 28, 5, 41,
15, "Camille Bojanski", "MI", 4.5, 49, 19, 43, 33, 30, 47, 32,
16, "Jonah Lebovitz", "MI", 4.0, 44, 42, 8, 22, 2, 34, 27,
17, "Kevin Cavin", "MI", 4.0, 32, 22, 55, 2, 56, 54, 5,
18, "Timothy McVay", "MI", 4.0, 28, 25, 1, 8, 60, 58, 51,
19, "Susan Drane", "MI", 4.0, 64, 15, 29, 50, 43, 4, 23,
20, "Gabriel Pistone", "MI", 4.0, 53, 43, 23, 19, 50, 2, 46,
21, "Andrew Ohl", "MI", 4.0, 61, 1, 56, 3, 37, 42, 6,
22, "Mark Sena", "ON", 4.0, 48, 17, 46, 16, 8, 56, 50,
23, "Seth Getz", "MI", 4.0, 4, 59, 20, 61, 13, 52, 8,
24, "Jonah Adler-Bell", "MI", 4.0, 64, 37, 36, 51, 55, 35, 52,
25, "Nicholas Strezev", "MI", 3.5, 35, 18, 3, 57, 54, 60, 11,
26, "Sabina Brunetti", "MI", 3.5, 55, 44, 32, 4, 57, 28, 53,
27, "Yun Ling", "MI", 3.5, 65, 30, 49, 10, 51, 6, 11,
28, "Piers Lochhead", "MI", 3.5, 18, 4, 58, 36, 14, 26, 53,
29, "Austin Wagner", "MI", 3.5, 44, 6, 19, 63, 41, 36, 10,
30, "Jorge Pereyra", "MI", 3.5, 50, 27, 36, 34, 15, 8, 48,
31, "Maria C Curi", "MI", 3.5, 34, 11, 10, 56, 9, 49, 47,
32, "Isaac Schwartz", "MI", 3.5, 17, 46, 26, 63, 36, 16, 14,
33, "Cody Burke", "MI", 3.5, 42, 59, 40, 15, 65, 9, 36,
34, "Zachary Drake", "MI", 3.0, 31, 6, 42, 30, 38, 16, 57,
35, "Brian Berger", "MI", 3.0, 25, 65, 47, 6, 50, 24, 9,
36, "Blaise Siebert", "MI", 3.0, 64, 53, 30, 28, 32, 29, 33,
37, "Joshua Hatton", "MI", 3.0, 45, 5, 53, 47, 21, 50, 52,
38, "Tom Furhmann", "MI", 3.0, 11, 56, 48, 46, 34, 59, 65,
39, "Myriana Lozano", "MI", 3.0, 1, 46, 52, 61, 66, 63, 47,
40, "Kenji Watanabe", "MI", 3.0, 54, 12, 33, 55, 64, 61, 59,
41, "Brent Orr", "MI", 3.0, 62, 50, 66, 29, 52, 57, 14,
42, "Ian Findlay", "MI", 3.0, 33, 16, 34, 54, 63, 21, 64,
43, "Rolf Mayer", "MI", 3.0, 56, 20, 15, 9, 19, 65, 13,
44, "John Upham", "MI", 2.5, 16, 26, 64, 28, 53, 45, 29,
45, "Marvin Hymers", "MI", 2.5, 5, 55, 57, 64, 12, 44, 66,
46, "Melvin Erickson", "MI", 2.5, 54, 2, 22, 38, 57, 10, 20,
47, "Phillip Nolan", "MI", 2.5, 64, 27, 35, 37, 65, 15, 39,
48, "Luc Martin", "MI", 2.5, 22, 9, 38, 62, 63, 49, 30,
49, "Farzad Roohparvar", "MI", 2.5, 15, 64, 27, 66, 55, 48, 35,
50, "Siddharth Vasanti", "MI", 2.0, 30, 41, 53, 19, 20, 37, 22,
51, "Tim McIntosh", "MI", 2.0, 62, 57, 9, 24, 27, 66, 18,
52, "Eli Rosenberg", "MI", 2.0, 57, 10, 39, 53, 41, 23, 37,
53, "Amanda Brocato", "MI", 2.0, 20, 36, 37, 52, 44, 60, 28,
54, "Luca Bernstein", "MI", 2.0, 40, 46, 62, 42, 25, 17, 13,
55, "Jose Flores", "MI", 2.0, 26, 45, 17, 40, 24, 63, 66,
56, "Jorge Ochoa", "MI", 2.0, 43, 38, 21, 31, 17, 22, 58,
57, "Abby Marshall", "MI", 2.0, 7, 8, 45, 25, 46, 41, 34,
58, "Terrence Loewe", "MI", 2.0, 2, 63, 28, 12, 64, 18, 56,
59, "Sarah Saenz", "MI", 1.5, 61, 33, 62, 38, 65, 38, 40,
60, "Dominique Roy", "MI", 1.5, 13, 64, 62, 66, 18, 53, 49,
61, "Brielle Kolnsberg", "MI", 1.0, 21, 3, 65, 39, 66, 40, 63,
62, "Robert Barron", "MI", 1.0, 41, 51, 59, 48, 65, 63, 42,
63, "Vivek Palaniappan", "MI", 1.0, 2, 58, 40, 29, 42, 39, 61,
64, "Joel Gonzalez Vargas", "MI", 1.0, 36, 19, 44, 45, 40, 50, 42,
65, "Garry Roudnikov", "MI", 1.0, 27, 35, 61, 33, 59, 43, 38,
66, "Lena Nummelin", "MI", 0.5, 39, 29, 41, 49, 61, 51, 45
)# Standard Elo expected score formula
# Source: Elo (1978); https://en.wikipedia.org/wiki/Elo_rating_system
elo_expected <- function(rating_a, rating_b) {
1 / (1 + 10^((rating_b - rating_a) / 400))
}
# For each player, sum expected scores across all 7 rounds (skip byes = 0)
opp_cols <- paste0("opp", 1:7)
results <- tournament %>%
rowwise() %>%
mutate(
expected_score = {
opps <- c_across(all_of(opp_cols))
opps <- opps[opps != 0] # remove byes
r_player <- pre_ratings[as.character(id)]
sum(sapply(opps, function(opp_id) {
r_opp <- pre_ratings[as.character(opp_id)]
elo_expected(r_player, r_opp)
}))
},
pre_rating = pre_ratings[as.character(id)]
) %>%
ungroup() %>%
mutate(
expected_score = round(expected_score, 2),
difference = round(actual_pts - expected_score, 2)
) %>%
select(id, name, state, pre_rating, actual_pts, expected_score, difference) %>%
arrange(desc(difference))Players whose actual score most exceeded their Elo-expected score.
top5_over <- results %>%
slice_max(difference, n = 5)
top5_over %>%
select(Name = name, State = state, `Pre-Rating` = pre_rating,
`Actual Score` = actual_pts, `Expected Score` = expected_score,
`Difference` = difference) %>%
kable(align = c("l","c","r","r","r","r")) %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) %>%
column_spec(6, bold = TRUE, color = "white", background = "#27ae60")| Name | State | Pre-Rating | Actual Score | Expected Score | Difference |
|---|---|---|---|---|---|
| Aditya Bajaj | MI | 1384 | 6.0 | 2.35 | 3.65 |
| Dakshesh Daruri | MI | 1553 | 6.0 | 3.70 | 2.30 |
| Isaac Schwartz | MI | 1186 | 3.5 | 1.23 | 2.27 |
| Anvit Rao | MI | 1365 | 5.0 | 3.24 | 1.76 |
| Timothy McVay | MI | 1449 | 4.0 | 2.78 | 1.22 |
Players whose actual score most fell short of their Elo-expected score.
top5_under <- results %>%
slice_min(difference, n = 5)
top5_under %>%
select(Name = name, State = state, `Pre-Rating` = pre_rating,
`Actual Score` = actual_pts, `Expected Score` = expected_score,
`Difference` = difference) %>%
kable(align = c("l","c","r","r","r","r")) %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) %>%
column_spec(6, bold = TRUE, color = "white", background = "#c0392b")| Name | State | Pre-Rating | Actual Score | Expected Score | Difference |
|---|---|---|---|---|---|
| Siddharth Vasanti | MI | 1745 | 2.0 | 5.92 | -3.92 |
| Dominique Roy | MI | 1399 | 1.5 | 4.66 | -3.16 |
| Myriana Lozano | MI | 1580 | 3.0 | 5.26 | -2.26 |
| Tim McIntosh | MI | 1419 | 2.0 | 4.03 | -2.03 |
| Kenji Watanabe | MI | 1416 | 3.0 | 4.85 | -1.85 |
All 66 players sorted by performance difference (overperformers first).
results %>%
mutate(Rank = row_number()) %>%
select(Rank, Name = name, State = state, `Pre-Rating` = pre_rating,
`Actual` = actual_pts, `Expected` = expected_score,
`Diff` = difference) %>%
kable(align = c("r","l","c","r","r","r","r")) %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = TRUE, font_size = 13) %>%
row_spec(which(results$difference >= 1.0), background = "#1a3a1a", color = "white") %>%
row_spec(which(results$difference <= -1.5), background = "#3a1a1a", color = "white")| Rank | Name | State | Pre-Rating | Actual | Expected | Diff |
|---|---|---|---|---|---|---|
| 1 | Aditya Bajaj | MI | 1384 | 6.0 | 2.35 | 3.65 |
| 2 | Dakshesh Daruri | MI | 1553 | 6.0 | 3.70 | 2.30 |
| 3 | Isaac Schwartz | MI | 1186 | 3.5 | 1.23 | 2.27 |
| 4 | Anvit Rao | MI | 1365 | 5.0 | 3.24 | 1.76 |
| 5 | Timothy McVay | MI | 1449 | 4.0 | 2.78 | 1.22 |
| 6 | Maria C Curi | MI | 1199 | 3.5 | 2.32 | 1.18 |
| 7 | Frederic Fournier | ON | 1411 | 5.0 | 3.86 | 1.14 |
| 8 | Zachary Drake | MI | 1175 | 3.0 | 1.94 | 1.06 |
| 9 | Mark Sena | ON | 1481 | 4.0 | 3.02 | 0.98 |
| 10 | Gabriel Pistone | MI | 1426 | 4.0 | 3.06 | 0.94 |
| 11 | Patrick H Schilling | MI | 1716 | 5.5 | 4.71 | 0.79 |
| 12 | Hanshi Zuo | MI | 1655 | 5.5 | 4.78 | 0.72 |
| 13 | Dylan Wang | MI | 1552 | 5.0 | 4.28 | 0.72 |
| 14 | Gary Hua | ON | 1794 | 6.0 | 5.33 | 0.67 |
| 15 | Simón Kazuko | MI | 1500 | 4.5 | 3.88 | 0.62 |
| 16 | Brian Berger | MI | 1321 | 3.0 | 2.39 | 0.61 |
| 17 | Gary Dee Swain | MI | 1649 | 5.0 | 4.57 | 0.43 |
| 18 | Jose Flores | MI | 1142 | 2.0 | 1.57 | 0.43 |
| 19 | Seth Getz | MI | 1436 | 4.0 | 3.58 | 0.42 |
| 20 | Camille Bojanski | MI | 1425 | 4.5 | 4.13 | 0.37 |
| 21 | Joel Gonzalez Vargas | MI | 980 | 1.0 | 0.71 | 0.29 |
| 22 | Gary Nirmalan | MI | 1552 | 4.5 | 4.25 | 0.25 |
| 23 | Tom Furhmann | MI | 1220 | 3.0 | 2.75 | 0.25 |
| 24 | Ian Findlay | MI | 1270 | 3.0 | 2.85 | 0.15 |
| 25 | Sawyer Smith | MI | 1579 | 4.5 | 4.50 | 0.00 |
| 26 | Abby Marshall | MI | 1227 | 2.0 | 2.03 | -0.03 |
| 27 | Phillip Nolan | MI | 1229 | 2.5 | 2.58 | -0.08 |
| 28 | Susan Drane | MI | 1552 | 4.0 | 4.15 | -0.15 |
| 29 | Bruce Harris | MI | 1641 | 5.0 | 5.17 | -0.17 |
| 30 | Marvin Hymers | MI | 1184 | 2.5 | 2.69 | -0.19 |
| 31 | Joshua Hatton | MI | 1391 | 3.0 | 3.20 | -0.20 |
| 32 | Rolf Mayer | MI | 1392 | 3.0 | 3.23 | -0.23 |
| 33 | Amanda Brocato | MI | 1279 | 2.0 | 2.26 | -0.26 |
| 34 | Austin Wagner | MI | 1480 | 3.5 | 3.93 | -0.43 |
| 35 | Farzad Roohparvar | MI | 1222 | 2.5 | 2.96 | -0.46 |
| 36 | Jorge Pereyra | MI | 1531 | 3.5 | 3.97 | -0.47 |
| 37 | Cody Burke | MI | 1363 | 3.5 | 3.97 | -0.47 |
| 38 | Hansen Song | OH | 1686 | 5.0 | 5.49 | -0.49 |
| 39 | Brent Orr | MI | 1387 | 3.0 | 3.67 | -0.67 |
| 40 | Sarah Saenz | MI | 1082 | 1.5 | 2.18 | -0.68 |
| 41 | Kevin Cavin | MI | 1552 | 4.0 | 4.69 | -0.69 |
| 42 | Brielle Kolnsberg | MI | 1149 | 1.0 | 1.73 | -0.73 |
| 43 | Andrew Ohl | MI | 1601 | 4.0 | 4.74 | -0.74 |
| 44 | John Upham | MI | 1356 | 2.5 | 3.27 | -0.77 |
| 45 | Blaise Siebert | MI | 1388 | 3.0 | 3.81 | -0.81 |
| 46 | Yun Ling | MI | 1525 | 3.5 | 4.35 | -0.85 |
| 47 | Piers Lochhead | MI | 1563 | 3.5 | 4.35 | -0.85 |
| 48 | Jonah Lebovitz | MI | 1604 | 4.0 | 4.90 | -0.90 |
| 49 | Robert Barron | MI | 1107 | 1.0 | 1.92 | -0.92 |
| 50 | Nicholas Strezev | MI | 1493 | 3.5 | 4.46 | -0.96 |
| 51 | Melvin Erickson | MI | 1403 | 2.5 | 3.70 | -1.20 |
| 52 | Sabina Brunetti | MI | 1520 | 3.5 | 4.82 | -1.32 |
| 53 | Garry Roudnikov | MI | 1153 | 1.0 | 2.32 | -1.32 |
| 54 | Luca Bernstein | MI | 1388 | 2.0 | 3.35 | -1.35 |
| 55 | Eli Rosenberg | MI | 1365 | 2.0 | 3.37 | -1.37 |
| 56 | Jorge Ochoa | MI | 1400 | 2.0 | 3.38 | -1.38 |
| 57 | Jonah Adler-Bell | MI | 1556 | 4.0 | 5.56 | -1.56 |
| 58 | Terrence Loewe | MI | 1436 | 2.0 | 3.58 | -1.58 |
| 59 | Lena Nummelin | MI | 1186 | 0.5 | 2.20 | -1.70 |
| 60 | Luc Martin | MI | 1413 | 2.5 | 4.22 | -1.72 |
| 61 | Vivek Palaniappan | MI | 1329 | 1.0 | 2.75 | -1.75 |
| 62 | Kenji Watanabe | MI | 1416 | 3.0 | 4.85 | -1.85 |
| 63 | Tim McIntosh | MI | 1419 | 2.0 | 4.03 | -2.03 |
| 64 | Myriana Lozano | MI | 1580 | 3.0 | 5.26 | -2.26 |
| 65 | Dominique Roy | MI | 1399 | 1.5 | 4.66 | -3.16 |
| 66 | Siddharth Vasanti | MI | 1745 | 2.0 | 5.92 | -3.92 |
library(ggplot2)
results_plot <- results %>%
mutate(name = factor(name, levels = rev(name)),
fill_color = case_when(
difference >= 1.0 ~ "Strong Over",
difference > 0 ~ "Slight Over",
difference > -1.0 ~ "Slight Under",
TRUE ~ "Strong Under"
))
ggplot(results_plot, aes(x = difference, y = name, fill = fill_color)) +
geom_col(width = 0.7) +
geom_vline(xintercept = 0, color = "white", linewidth = 0.4) +
scale_fill_manual(values = c(
"Strong Over" = "#27ae60",
"Slight Over" = "#82c99a",
"Slight Under" = "#e08080",
"Strong Under" = "#c0392b"
)) +
labs(
title = "Chess Tournament: Actual vs. Expected Score (Elo)",
subtitle = "Difference = Actual Score − Expected Score",
x = "Difference (points)",
y = NULL,
fill = NULL,
caption = "Formula: E = 1 / (1 + 10^((R_opp − R_player)/400))\nSource: Elo (1978); Wikipedia — Elo rating system"
) +
theme_minimal(base_size = 10) +
theme(
plot.background = element_rect(fill = "#1a1a2e", color = NA),
panel.background = element_rect(fill = "#1a1a2e", color = NA),
panel.grid.major.y = element_blank(),
panel.grid.major.x = element_line(color = "#333355"),
panel.grid.minor = element_blank(),
text = element_text(color = "#e8e0d0"),
axis.text = element_text(color = "#aaaaaa", size = 8),
plot.title = element_text(size = 13, face = "bold", color = "#c8a96e"),
plot.subtitle = element_text(size = 10, color = "#888888"),
plot.caption = element_text(size = 8, color = "#555555"),
legend.position = "top",
legend.text = element_text(size = 9)
)