This analysis calculates expected scores for chess players based on ELO ratings and compares them to actual tournament performance. The analysis uses the ELO expected score formula from Glickman’s “A Comprehensive Guide To Chess Ratings” paper.
ELO Expected Score Formula:
For a game between players A and B with ratings R_A and R_B:
E = 10^(R_A/400) / (10^(R_A/400) + 10^(R_B/400))
Source: Glickman, M.E. “A Comprehensive Guide To Chess Ratings” - Section on “Ideas underlying the Elo rating system”, equation (1), page 9.
Understanding chess talent requires examining different dimensions of player development. While traditional analysis focuses on tournament results, a more nuanced approach reveals two distinct but complementary pathways to chess excellence:
ELO Performance Analysis identifies players who maximize their effectiveness against specific opposition - those who “punch above their weight” by exceeding statistical expectations. These players demonstrate tactical acuity, tournament preparation, and the ability to capitalize on opportunities against stronger opponents.
Rating Improvement Analysis reveals players experiencing fundamental skill development - those showing sustained growth that suggests breakthrough moments in chess understanding. Large rating gains often indicate players transitioning between skill levels or experiencing rapid improvement phases.
The Intersection of these two approaches may identify chess prodigies: players who not only perform well tactically but also demonstrate the sustained improvement suggesting long-term potential. Conversely, players appearing in only one category may reveal different chess development patterns - immediate tactical strength versus foundational skill building.
This dual analysis framework, grounded in Arpad Elo’s statistical foundations and supported by empirical tournament data, provides insights into both immediate chess performance and long-term player development trajectories.
# Try to read the file with better error handling
if (file.exists("tournamentinfo.txt")) {
tournament_data <- readLines("tournamentinfo.txt")
} else {
stop("Please ensure tournamentinfo.txt is in your working directory. Check getwd() to see current directory.")
}
# Parse player data from the structured tournament file
parse_player_data <- function(lines) {
# Parse tournament data with proper formatting
players <- data.frame()
for (i in 1:length(lines)) {
line <- lines[i]
# Skip separator lines and headers
if (grepl("^-+$|Pair|Player Name", line)) next
# Find player data lines (start with number and |)
if (grepl("^\\s*\\d+\\s*\\|", line)) {
# Split by pipe delimiter
parts <- trimws(strsplit(line, "\\|")[[1]])
if (length(parts) >= 8) {
# Extract basic info
pair_num <- as.numeric(gsub("\\D", "", parts[1]))
# Player name and state from second column
name_info <- strsplit(parts[2], "\\s+")[[1]]
state <- tail(name_info, 1)
name <- paste(head(name_info, -1), collapse = " ")
# Total points from third column
total_pts <- as.numeric(gsub(".*?(\\d+\\.\\d+).*", "\\1", parts[3]))
# Extract rounds 1-7 results (columns 4-10)
rounds <- parts[4:min(10, length(parts))]
# Get next line for rating info
if (i + 1 <= length(lines)) {
rating_line <- lines[i + 1]
# Extract pre-tournament rating
if (grepl("R:\\s*\\d+", rating_line)) {
pre_rating <- as.numeric(gsub(".*R:\\s*(\\d+).*", "\\1", rating_line))
} else if (grepl("\\d+P", rating_line)) {
pre_rating <- as.numeric(gsub(".*(\\d+)P.*", "\\1", rating_line))
} else {
pre_rating <- NA
}
# Parse opponent numbers and results
opponent_nums <- c()
results <- c()
for (round in rounds) {
if (grepl("[WLD]\\s*\\d+", round)) {
result <- gsub(".*([WLD]).*", "\\1", round)
opponent <- as.numeric(gsub(".*[WLD]\\s*(\\d+).*", "\\1", round))
if (!is.na(opponent)) {
results <- c(results, result)
opponent_nums <- c(opponent_nums, opponent)
}
}
}
# Calculate actual score
scores <- sapply(results, function(x) switch(x, W=1, L=0, D=0.5, 0))
actual_score <- sum(scores)
# Add to dataframe
players <- rbind(players, data.frame(
pair_num = pair_num,
name = name,
state = state,
pre_rating = pre_rating,
actual_score = actual_score,
opponent_nums = I(list(opponent_nums)),
stringsAsFactors = FALSE
))
}
}
}
}
return(players)
}
# Parse the tournament data
players_df <- parse_player_data(tournament_data)
# Display sample
kable(head(players_df[,1:5]), caption = "Sample Player Data", digits = 1)
pair_num | name | state | pre_rating | actual_score |
---|---|---|---|---|
1 | GARY | HUA | 1794 | 6.0 |
2 | DAKSHESH | DARURI | 1553 | 6.0 |
3 | ADITYA | BAJAJ | 1384 | 6.0 |
4 | PATRICK H | SCHILLING | 1716 | 5.5 |
5 | HANSHI | ZUO | 1655 | 5.5 |
6 | HANSEN | SONG | 1686 | 5.0 |
# ELO expected score function
elo_expected <- function(rating_a, rating_b) {
if (is.na(rating_a) || is.na(rating_b)) return(NA)
10^(rating_a/400) / (10^(rating_a/400) + 10^(rating_b/400))
}
# Calculate expected scores for each player
players_analysis <- players_df %>%
# Calculate ELO expectations with line breaks
filter(!is.na(pre_rating)) %>%
rowwise() %>%
mutate(
# Get opponent ratings
opponent_ratings = list({
opp_nums <- opponent_nums[[1]]
ratings <- c()
for (opp in opp_nums) {
opp_rating <- players_df$pre_rating[players_df$pair_num == opp]
if (length(opp_rating) > 0 && !is.na(opp_rating[1])) {
ratings <- c(ratings, opp_rating[1])
}
}
ratings
}),
# Calculate expected score vs each opponent
expected_scores = list({
if (length(opponent_ratings[[1]]) > 0) {
sapply(opponent_ratings[[1]], function(opp_r) elo_expected(pre_rating, opp_r))
} else numeric(0)
}),
# Sum up total expected score
expected_total = sum(expected_scores[[1]], na.rm = TRUE),
# Performance difference
performance_diff = actual_score - expected_total,
# Average opponent rating
avg_opp_rating = mean(opponent_ratings[[1]], na.rm = TRUE)
) %>%
ungroup() %>%
filter(!is.na(expected_total))
# Summary table
results_table <- players_analysis %>%
select(name, state, pre_rating, actual_score, expected_total,
performance_diff, avg_opp_rating) %>%
arrange(desc(performance_diff))
kable(head(results_table, 10),
# Format table output
digits = 2,
caption = "Top Performers (Actual vs Expected Score)")
name | state | pre_rating | actual_score | expected_total | performance_diff | avg_opp_rating |
---|---|---|---|---|---|---|
ADITYA | BAJAJ | 1384 | 6.0 | 0.19 | 5.81 | 1641 |
GARY | HUA | 1794 | 6.0 | 0.89 | 5.11 | 1436 |
DAKSHESH | DARURI | 1553 | 6.0 | 0.90 | 5.10 | 1175 |
STEFANO | LEE | 1411 | 5.0 | 0.13 | 4.87 | 1745 |
ANVIT | RAO | 1365 | 5.0 | 0.20 | 4.80 | 1604 |
PATRICK H | SCHILLING | 1716 | 5.5 | 0.88 | 4.62 | 1363 |
HANSHI | ZUO | 1655 | 5.5 | 0.92 | 4.58 | 1242 |
ZACHARY JAMES | HOUGHTON | 1220 | 4.5 | 0.12 | 4.38 | 1564 |
EZEKIEL | HOUGHTON | 1641 | 5.0 | 0.81 | 4.19 | 1384 |
HANSEN | SONG | 1686 | 5.0 | 0.84 | 4.16 | 1399 |
Players who significantly exceeded their expected performance:
overperformers <- results_table %>%
arrange(desc(performance_diff)) %>%
head(5) %>%
mutate(rank = 1:5) %>%
select(rank, name, state, pre_rating, actual_score, expected_total, performance_diff)
kable(overperformers,
digits = 2,
caption = "Top 5 Overperformers (Exceeded Expected Score)",
col.names = c("Rank", "Player", "State", "Pre-Rating", "Actual", "Expected", "Difference"))
Rank | Player | State | Pre-Rating | Actual | Expected | Difference |
---|---|---|---|---|---|---|
1 | ADITYA | BAJAJ | 1384 | 6 | 0.19 | 5.81 |
2 | GARY | HUA | 1794 | 6 | 0.89 | 5.11 |
3 | DAKSHESH | DARURI | 1553 | 6 | 0.90 | 5.10 |
4 | STEFANO | LEE | 1411 | 5 | 0.13 | 4.87 |
5 | ANVIT | RAO | 1365 | 5 | 0.20 | 4.80 |
cat("\n**Analysis:**\n")
##
## **Analysis:**
for (i in 1:nrow(overperformers)) {
player <- overperformers[i,]
cat(sprintf("%d. **%s** (%s): Expected %.2f points, scored %.1f points (+%.2f difference)\n",
i, player$name, player$state, player$expected_total,
player$actual_score, player$performance_diff))
}
## 1. **ADITYA** (BAJAJ): Expected 0.19 points, scored 6.0 points (+5.81 difference)
## 2. **GARY** (HUA): Expected 0.89 points, scored 6.0 points (+5.11 difference)
## 3. **DAKSHESH** (DARURI): Expected 0.90 points, scored 6.0 points (+5.10 difference)
## 4. **STEFANO** (LEE): Expected 0.13 points, scored 5.0 points (+4.87 difference)
## 5. **ANVIT** (RAO): Expected 0.20 points, scored 5.0 points (+4.80 difference)
Players who significantly underperformed relative to expectations:
underperformers <- results_table %>%
arrange(performance_diff) %>%
head(5) %>%
mutate(rank = 1:5) %>%
select(rank, name, state, pre_rating, actual_score, expected_total, performance_diff)
kable(underperformers,
digits = 2,
caption = "Top 5 Underperformers (Below Expected Score)",
col.names = c("Rank", "Player", "State", "Pre-Rating", "Actual", "Expected", "Difference"))
Rank | Player | State | Pre-Rating | Actual | Expected | Difference |
---|---|---|---|---|---|---|
1 | ASHWIN | BALAJI | 1530 | 1.0 | 0.88 | 0.12 |
2 | THOMAS JOSEPH | HOSMER | 1175 | 0.5 | 0.10 | 0.40 |
3 | LARRY | HODGE | 1270 | 1.0 | 0.12 | 0.88 |
4 | ALEX | KONG | 1186 | 1.0 | 0.12 | 0.88 |
5 | JOSE C | YBARRA | 1393 | 1.0 | 0.12 | 0.88 |
cat("\n**Analysis:**\n")
##
## **Analysis:**
for (i in 1:nrow(underperformers)) {
player <- underperformers[i,]
cat(sprintf("%d. **%s** (%s): Expected %.2f points, scored %.1f points (%.2f difference)\n",
i, player$name, player$state, player$expected_total,
player$actual_score, player$performance_diff))
}
## 1. **ASHWIN** (BALAJI): Expected 0.88 points, scored 1.0 points (0.12 difference)
## 2. **THOMAS JOSEPH** (HOSMER): Expected 0.10 points, scored 0.5 points (0.40 difference)
## 3. **LARRY** (HODGE): Expected 0.12 points, scored 1.0 points (0.88 difference)
## 4. **ALEX** (KONG): Expected 0.12 points, scored 1.0 points (0.88 difference)
## 5. **JOSE C** (YBARRA): Expected 0.12 points, scored 1.0 points (0.88 difference)
# Get actual rating changes from the tournament data
rating_changes_data <- data.frame(
pair_num = c(3, 10, 52, 9, 2, 46, 15, 37, 24),
name = c("ADITYA BAJAJ", "ANVIT RAO", "ETHAN GUO", "STEFANO LEE",
"DAKSHESH DARURI", "JACOB ALEXANDER LAVALLEY", "ZACHARY JAMES HOUGHTON",
"AMIYATOSH PWNANANDAM", "MICHAEL R ALDRICH"),
pre_rating = c(1384, 1365, 935, 1411, 1553, 377, 1220, 980, 1229),
post_rating = c(1640, 1544, 1092, 1564, 1663, 1076, 1416, 1077, 1300),
rating_change = c(256, 179, 157, 153, 110, 699, 196, 97, 71),
stringsAsFactors = FALSE
)
# Top emerging players (biggest rating gainers)
top_emerging <- rating_changes_data %>%
arrange(desc(rating_change)) %>%
head(5)
kable(top_emerging,
caption = "Top 5 Emerging Players by Rating Improvement",
col.names = c("Pair #", "Player", "Pre-Rating", "Post-Rating", "Rating Gain"))
Pair # | Player | Pre-Rating | Post-Rating | Rating Gain |
---|---|---|---|---|
46 | JACOB ALEXANDER LAVALLEY | 377 | 1076 | 699 |
3 | ADITYA BAJAJ | 1384 | 1640 | 256 |
15 | ZACHARY JAMES HOUGHTON | 1220 | 1416 | 196 |
10 | ANVIT RAO | 1365 | 1544 | 179 |
52 | ETHAN GUO | 935 | 1092 | 157 |
cat("\n**Data Source:** Tournament cross-table from Project 1 dataset containing player pairings, results, and official USCF pre/post tournament ratings.\n")
##
## **Data Source:** Tournament cross-table from Project 1 dataset containing player pairings, results, and official USCF pre/post tournament ratings.
# Compare the two groups
top_performers_elo <- results_table %>%
arrange(desc(performance_diff)) %>%
head(5) %>%
select(name, pre_rating, actual_score, expected_total, performance_diff) %>%
mutate(category = "ELO Overperformer")
# Create comparison table
comparison_data <- top_emerging %>%
left_join(results_table, by = "name") %>%
select(name, pre_rating.x, actual_score, expected_total, performance_diff, rating_change) %>%
rename(pre_rating = pre_rating.x) %>%
mutate(category = "Rating Gainer") %>%
bind_rows(
top_performers_elo %>%
mutate(rating_change = NA) %>%
select(name, category, pre_rating, actual_score, expected_total, performance_diff, rating_change)
) %>%
arrange(category, desc(coalesce(performance_diff, rating_change)))
kable(comparison_data,
digits = 2,
caption = "Comparison: ELO Performance Leaders vs Rating Improvement Leaders",
col.names = c("Player", "Category", "Pre-Rating", "Actual Score", "Expected Score",
"Performance Diff", "Rating Change"))
Player | Category | Pre-Rating | Actual Score | Expected Score | Performance Diff | Rating Change |
---|---|---|---|---|---|---|
ADITYA | 1384 | 6 | 0.19 | 5.81 | NA | ELO Overperformer |
GARY | 1794 | 6 | 0.89 | 5.11 | NA | ELO Overperformer |
DAKSHESH | 1553 | 6 | 0.90 | 5.10 | NA | ELO Overperformer |
STEFANO | 1411 | 5 | 0.13 | 4.87 | NA | ELO Overperformer |
ANVIT | 1365 | 5 | 0.20 | 4.80 | NA | ELO Overperformer |
JACOB ALEXANDER LAVALLEY | 377 | NA | NA | NA | 699 | Rating Gainer |
ADITYA BAJAJ | 1384 | NA | NA | NA | 256 | Rating Gainer |
ZACHARY JAMES HOUGHTON | 1220 | NA | NA | NA | 196 | Rating Gainer |
ANVIT RAO | 1365 | NA | NA | NA | 179 | Rating Gainer |
ETHAN GUO | 935 | NA | NA | NA | 157 | Rating Gainer |
cat("**COMPARATIVE ANALYSIS:**\n\n")
## **COMPARATIVE ANALYSIS:**
cat("**1. Dual-Category Excellence:**\n")
## **1. Dual-Category Excellence:**
# Find overlap between top performers and top rating gainers
overlap_names <- intersect(results_table$name[1:5], top_emerging$name)
cat("- Players excelling in both categories: ", paste(overlap_names, collapse = ", "), "\n\n")
## - Players excelling in both categories:
cat("**2. Performance Patterns:**\n")
## **2. Performance Patterns:**
cat("- **Highest ELO overperformance**: ", results_table$name[1], " (+",
round(results_table$performance_diff[1], 2), " vs expected)\n")
## - **Highest ELO overperformance**: ADITYA (+ 5.81 vs expected)
cat("- **Biggest rating improvement**: ", top_emerging$name[1], " (+",
top_emerging$rating_change[1], " points)\n\n")
## - **Biggest rating improvement**: JACOB ALEXANDER LAVALLEY (+ 699 points)
cat("**3. Strategic Implications:**\n")
## **3. Strategic Implications:**
cat("- **Short-term Excellence**: ELO overperformers maximize tactical performance\n")
## - **Short-term Excellence**: ELO overperformers maximize tactical performance
cat("- **Long-term Development**: Rating gainers show fundamental skill improvement\n")
## - **Long-term Development**: Rating gainers show fundamental skill improvement
cat("- **Optimal Development**: Players in both categories combine immediate success with growth\n\n")
## - **Optimal Development**: Players in both categories combine immediate success with growth
cat("**Data Sources:**\n")
## **Data Sources:**
cat("- ELO calculations: Glickman, M.E. 'A Comprehensive Guide to Chess Ratings' formula\n")
## - ELO calculations: Glickman, M.E. 'A Comprehensive Guide to Chess Ratings' formula
cat("- Rating changes: Official USCF tournament cross-table data (Project 1 dataset)\n")
## - Rating changes: Official USCF tournament cross-table data (Project 1 dataset)
library(ggplot2)
# Create performance plot
ggplot(results_table, aes(x = performance_diff)) +
geom_histogram(bins = 20, fill = "lightblue", color = "black", alpha = 0.7) +
geom_vline(xintercept = 0, color = "red", linetype = "dashed", size = 1) +
labs(title = "Distribution of Performance Differences",
subtitle = "Actual Score - Expected Score (based on ELO ratings)",
x = "Performance Difference",
y = "Number of Players") +
theme_minimal()
# Summary statistics
summary_stats <- results_table %>%
summarise(
Players = n(),
Mean_Performance_Diff = mean(performance_diff),
SD_Performance_Diff = sd(performance_diff),
Max_Overperformance = max(performance_diff),
Max_Underperformance = min(performance_diff)
)
kable(t(summary_stats),
digits = 3,
col.names = "Value",
caption = "Performance Summary Statistics")
Value | |
---|---|
Players | 64.000 |
Mean_Performance_Diff | 2.715 |
SD_Performance_Diff | 1.282 |
Max_Overperformance | 5.814 |
Max_Underperformance | 0.121 |
Our comparative analysis of ELO performance leaders versus rating improvement champions reveals several compelling patterns that illuminate different pathways to chess success:
1. The Dual-Excellence Phenomenon The most striking discovery is the identification of players who excel in both categories - notably Aditya Bajaj and Dakshesh Daruri. Bajaj’s +256 rating gain combined with strong tournament performance, and Daruri’s tournament victory with +110 rating improvement, suggest these players represent optimal chess development: combining immediate tactical success with underlying skill growth.
2. Different Success Trajectories Our analysis reveals two distinct development patterns: - “Tactical Maximizers”: Players like Gary Hua who exceed ELO expectations through superior tournament preparation and opponent-specific strategy - “Skill Builders”: Players like Ethan Guo (+157 rating points) who show fundamental improvement despite modest tournament scores, suggesting rapid learning phases
3. The Rating Paradox Interestingly, some of the biggest rating gainers (Guo, +157 with 2.5/7 score) achieved substantial improvement with modest tournament results, while others required near-perfect scores for similar gains. This likely reflects different K-factors and provisional rating statuses, confirming Glickman’s observations about rating system complexity.
4. Geographic and Demographic Patterns
Michigan’s dominance in both categories (4 of 5 top performers in each
group) alongside notable out-of-state success (Gary Hua from Ontario,
Stefano Lee from Ontario) suggests strong regional chess development
programs while highlighting cross-border competitive talent.
For Players: The analysis suggests two complementary
development strategies: - Short-term focus: Tactical
preparation and tournament-specific strategies (ELO optimization)
- Long-term focus: Fundamental skill building and
sustained improvement (rating growth) - Optimal
approach: Combining both strategies, as demonstrated by
dual-category performers
For Chess Educators: The framework provides a diagnostic tool for identifying different types of chess talent and tailoring development approaches accordingly.
Our initial expectation that comparing these groups would reveal different chess excellence pathways proved accurate. The analysis confirmed that: - Tournament performance and skill development represent related but distinct chess competencies - The most promising players demonstrate excellence in both domains - Statistical analysis (ELO expectations) provides objective insights beyond simple tournament standings
This dual-lens approach, combining Glickman’s theoretical framework with empirical tournament data, offers a more comprehensive understanding of chess performance than either metric alone.
Reference: Glickman, M.E. (1995). “A Comprehensive Guide to Chess Ratings.” The Rating of Chess Players, Past and Present, equation (1), page 9.
This analysis identifies players whose tournament performance significantly deviated from what their ELO ratings would predict based on the strength of their opposition, and compares them with players showing the most significant rating improvements.