Last active
June 9, 2026 19:41
-
-
Save cavedave/7ef846bf5c47cd0af1871b9a3414229f to your computer and use it in GitHub Desktop.
Karpov Kasparov first match starting in 1984. Was Karpov really 'a boxer on the ropes' at the end. I wanted to see if a computer analysis showed he was. Data from lichess https://lichess.org/study/wgJ634pR/SgIZNfgR It looks to me that Karpov was playing worse by the end
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Game | kasp_accuracy | kasp_ACPL | kasp_inac | kasp_mistake | kasp_blun | karp_accuracy | karp_ACPL | karp_inac | karp_mistake | karp_blun | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 93 | 19 | 3 | 0 | 0 | 94 | 18 | 1 | 0 | 0 | |
| 2 | 83 | 40 | 3 | 1 | 2 | 84 | 40 | 4 | 3 | 1 | |
| 3 | 87 | 36 | 4 | 1 | 0 | 95 | 14 | 0 | 1 | 0 | |
| 4 | 97 | 9 | 0 | 0 | 0 | 96 | 10 | 1 | 0 | 0 | |
| 5 | 97 | 8 | 0 | 0 | 0 | 96 | 12 | 0 | 0 | 0 | |
| 6 | 88 | 38 | 7 | 2 | 2 | 92 | 31 | 5 | 2 | 1 | |
| 7 | 89 | 27 | 1 | 0 | 1 | 94 | 17 | 0 | 1 | 0 | |
| 8 | 96 | 8 | 1 | 0 | 0 | 96 | 9 | 1 | 0 | 0 | |
| 9 | 90 | 28 | 1 | 2 | 2 | 92 | 18 | 1 | 2 | 1 | |
| 10 | 93 | 14 | 1 | 0 | 0 | 94 | 13 | 0 | 0 | 0 | |
| 11 | 98 | 6 | 1 | 0 | 0 | 97 | 7 | 1 | 0 | 0 | |
| 12 | 98 | 6 | 0 | 0 | 0 | 98 | 5 | 0 | 0 | 0 | |
| 13 | 98 | 6 | 0 | 0 | 0 | 98 | 6 | 0 | 0 | 0 | |
| 14 | 97 | 8 | 0 | 0 | 0 | 97 | 9 | 0 | 0 | 0 | |
| 15 | 98 | 5 | 0 | 0 | 0 | 98 | 5 | 0 | 0 | 0 | |
| 16 | 89 | 22 | 1 | 1 | 2 | 93 | 16 | 0 | 2 | 0 | |
| 17 | 98 | 5 | 0 | 0 | 0 | 98 | 6 | 0 | 0 | 0 | |
| 18 | 98 | 6 | 0 | 0 | 0 | 98 | 7 | 0 | 0 | 0 | |
| 19 | 97 | 7 | 1 | 0 | 0 | 98 | 6 | 0 | 0 | 0 | |
| 20 | 96 | 8 | 0 | 0 | 0 | 96 | 10 | 0 | 0 | 0 | |
| 21 | 98 | 7 | 0 | 0 | 0 | 97 | 8 | 0 | 0 | 0 | |
| 22 | 96 | 10 | 0 | 0 | 0 | 96 | 10 | 0 | 0 | 0 | |
| 23 | 97 | 6 | 0 | 0 | 0 | 98 | 6 | 0 | 0 | 0 | |
| 24 | 95 | 14 | 0 | 0 | 0 | 93 | 16 | 0 | 0 | 0 | |
| 25 | 85 | 20 | 0 | 2 | 0 | 91 | 11 | 0 | 1 | 0 | |
| 26 | 96 | 10 | 0 | 0 | 0 | 96 | 9 | 0 | 0 | 0 | |
| 27 | 92 | 25 | 3 | 0 | 1 | 96 | 13 | 1 | 0 | 0 | |
| 28 | 95 | 13 | 0 | 0 | 0 | 95 | 11 | 1 | 0 | 0 | |
| 29 | 97 | 9 | 0 | 0 | 0 | 98 | 6 | 0 | 0 | 0 | |
| 30 | 95 | 12 | 1 | 0 | 0 | 96 | 9 | 0 | 0 | 0 | |
| 31 | 96 | 10 | 0 | 0 | 0 | 96 | 11 | 1 | 0 | 0 | |
| 32 | 89 | 30 | 4 | 2 | 1 | 89 | 33 | 9 | 1 | 0 | |
| 33 | 94 | 12 | 1 | 0 | 0 | 95 | 12 | 1 | 0 | 0 | |
| 34 | 97 | 7 | 0 | 0 | 0 | 98 | 6 | 0 | 0 | 0 | |
| 35 | 93 | 18 | 0 | 0 | 0 | 92 | 19 | 1 | 0 | 0 | |
| 36 | 91 | 19 | 2 | 0 | 1 | 90 | 20 | 1 | 1 | 1 | |
| 37 | 95 | 10 | 0 | 0 | 0 | 94 | 13 | 0 | 0 | 0 | |
| 38 | 83 | 27 | 0 | 1 | 1 | 84 | 26 | 0 | 1 | 1 | |
| 39 | 97 | 8 | 1 | 0 | 0 | 97 | 8 | 0 | 0 | 0 | |
| 40 | 97 | 8 | 1 | 0 | 0 | 97 | 8 | 1 | 0 | 0 | |
| 41 | 95 | 9 | 0 | 0 | 1 | 95 | 9 | 1 | 0 | 1 | |
| 42 | 98 | 6 | 0 | 0 | 0 | 98 | 5 | 0 | 0 | 0 | |
| 43 | 97 | 7 | 0 | 0 | 0 | 96 | 10 | 0 | 0 | 0 | |
| 44 | 94 | 15 | 3 | 0 | 0 | 94 | 15 | 2 | 0 | 0 | |
| 45 | 97 | 8 | 0 | 0 | 0 | 96 | 9 | 1 | 0 | 0 | |
| 46 | 89 | 25 | 2 | 1 | 2 | 90 | 23 | 4 | 1 | 1 | |
| 47 | 97 | 11 | 0 | 0 | 0 | 86 | 35 | 2 | 0 | 1 | |
| 48 | 95 | 17 | 2 | 1 | 0 | 93 | 23 | 6 | 0 | 0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Plot per-game ACPL for the 1984 Karpov vs Kasparov match (Tufte style).""" | |
| from __future__ import annotations | |
| import csv | |
| import re | |
| import sys | |
| from pathlib import Path | |
| import matplotlib.pyplot as plt | |
| from matplotlib.transforms import blended_transform_factory | |
| HEADER_RE = re.compile(r"^\[(\w+)\s+\"(.*)\"\]\s*$", re.MULTILINE) | |
| TUFTE_BG = "#fffff8" | |
| TUFTE_TEXT = "#111111" | |
| TUFTE_AXIS = "#cccccc" | |
| TUFTE_GRAY = "#666666" | |
| TUFTE_KARP = "#e15759" | |
| TUFTE_KASP = "#4e79a7" | |
| SERIF = ["ET Book", "Palatino Linotype", "Palatino", "Book Antiqua", "Georgia", "DejaVu Serif"] | |
| DEFAULT_CSV = "1984_match_analysis.csv" | |
| DEFAULT_PGN = "lichess_study_1984-world-championship-karpov-vs-kasparov_by_Lichess_2021.11.29.pgn" | |
| DEFAULT_OUT = "graphs/acplraw.png" | |
| def load_rows(path: Path) -> list[dict]: | |
| with path.open(encoding="utf-8") as f: | |
| return list(csv.DictReader(f)) | |
| def load_game_winners(pgn_path: Path) -> dict[int, str | None]: | |
| """Map game number to 'Karpov', 'Kasparov', or None for draws.""" | |
| text = pgn_path.read_text(encoding="utf-8") | |
| winners: dict[int, str | None] = {} | |
| for chunk in re.split(r"\n\n(?=\[Event )", text.strip()): | |
| headers = dict(HEADER_RE.findall(chunk)) | |
| round_num = int(headers.get("Round", 0)) | |
| if not round_num: | |
| continue | |
| white = headers.get("White", "") | |
| black = headers.get("Black", "") | |
| result = headers.get("Result", "") | |
| if result == "1-0": | |
| winners[round_num] = "Karpov" if white.startswith("Karpov") else "Kasparov" | |
| elif result == "0-1": | |
| winners[round_num] = "Karpov" if black.startswith("Karpov") else "Kasparov" | |
| else: | |
| winners[round_num] = None | |
| return winners | |
| def _tufte_rc() -> None: | |
| plt.rcParams.update( | |
| { | |
| "font.family": "serif", | |
| "font.serif": SERIF, | |
| "axes.titlesize": 14, | |
| "axes.labelsize": 11, | |
| "xtick.labelsize": 11, | |
| "ytick.labelsize": 11, | |
| "axes.labelcolor": TUFTE_TEXT, | |
| "text.color": TUFTE_TEXT, | |
| "axes.edgecolor": TUFTE_AXIS, | |
| "xtick.color": TUFTE_TEXT, | |
| "ytick.color": TUFTE_TEXT, | |
| } | |
| ) | |
| def _direct_label( | |
| ax: plt.Axes, x: float, y: float, text: str, color: str, dy: float = 0 | |
| ) -> None: | |
| ax.annotate( | |
| text, | |
| xy=(x, y), | |
| xytext=(6, dy), | |
| textcoords="offset points", | |
| fontsize=11, | |
| color=color, | |
| va="center", | |
| fontfamily="serif", | |
| ) | |
| def _plot_match_average_axis_marks( | |
| ax: plt.Axes, | |
| karp_match_avg: float, | |
| kasp_match_avg: float, | |
| ) -> None: | |
| yaxis_marks = blended_transform_factory(ax.transAxes, ax.transData) | |
| tick_len = 0.018 | |
| for match_avg, color in ((karp_match_avg, TUFTE_KARP), (kasp_match_avg, TUFTE_KASP)): | |
| ax.plot( | |
| [-tick_len, 0], | |
| [match_avg, match_avg], | |
| transform=yaxis_marks, | |
| color=color, | |
| linewidth=2.5, | |
| solid_capstyle="butt", | |
| clip_on=False, | |
| zorder=5, | |
| ) | |
| mid = (karp_match_avg + kasp_match_avg) / 2 | |
| ax.text( | |
| -tick_len - 0.014, | |
| mid, | |
| "Match\nAverage", | |
| transform=yaxis_marks, | |
| fontsize=9, | |
| fontfamily="sans-serif", | |
| color=TUFTE_GRAY, | |
| ha="right", | |
| va="center", | |
| linespacing=0.9, | |
| clip_on=False, | |
| ) | |
| def plot_acpl_raw( | |
| games: list[int], | |
| karp: list[int], | |
| kasp: list[int], | |
| out_path: Path, | |
| winners: dict[int, str | None] | None = None, | |
| ) -> None: | |
| """Per-game ACPL chart — Tufte style, no running average.""" | |
| _tufte_rc() | |
| karp_f = [float(v) for v in karp] | |
| kasp_f = [float(v) for v in kasp] | |
| karp_match_avg = sum(karp_f) / len(karp_f) | |
| kasp_match_avg = sum(kasp_f) / len(kasp_f) | |
| winners = winners or {} | |
| fig, ax = plt.subplots(figsize=(10, 5), facecolor=TUFTE_BG) | |
| ax.set_facecolor(TUFTE_BG) | |
| ax.plot(games, karp_f, color=TUFTE_KARP, linewidth=2, solid_capstyle="round", zorder=3) | |
| ax.plot(games, kasp_f, color=TUFTE_KASP, linewidth=2, solid_capstyle="round", zorder=3) | |
| karp_win_x = [g for g in games if winners.get(g) == "Karpov"] | |
| kasp_win_x = [g for g in games if winners.get(g) == "Kasparov"] | |
| if karp_win_x: | |
| karp_win_y = [karp_f[games.index(g)] for g in karp_win_x] | |
| ax.scatter( | |
| karp_win_x, | |
| karp_win_y, | |
| s=42, | |
| color=TUFTE_KARP, | |
| edgecolors=TUFTE_BG, | |
| linewidths=1.2, | |
| zorder=5, | |
| ) | |
| if kasp_win_x: | |
| kasp_win_y = [kasp_f[games.index(g)] for g in kasp_win_x] | |
| ax.scatter( | |
| kasp_win_x, | |
| kasp_win_y, | |
| s=42, | |
| color=TUFTE_KASP, | |
| edgecolors=TUFTE_BG, | |
| linewidths=1.2, | |
| zorder=5, | |
| ) | |
| _plot_match_average_axis_marks(ax, karp_match_avg, kasp_match_avg) | |
| last = games[-1] | |
| _direct_label(ax, last, karp_f[-1], "Karpov", TUFTE_KARP, dy=8) | |
| _direct_label(ax, last, kasp_f[-1], "Kasparov", TUFTE_KASP, dy=-8) | |
| x_data_max = games[-1] | |
| x_plot_max = games[-1] + 2 | |
| y_min = min(karp_f + kasp_f) - 2 | |
| y_max = max(karp_f + kasp_f) + 4 | |
| ax.spines["top"].set_visible(False) | |
| ax.spines["right"].set_visible(False) | |
| ax.spines["bottom"].set_bounds(games[0], x_data_max) | |
| ax.spines["left"].set_bounds(y_min, y_max) | |
| ax.set_xlim(games[0], x_plot_max) | |
| ax.set_ylim(y_min, y_max) | |
| ax.set_xlabel("Game", fontfamily="sans-serif") | |
| ax.set_ylabel("Centipawn loss", fontfamily="sans-serif") | |
| x_ticks = list(range(games[0], games[-1] + 1, 4)) | |
| if games[-1] not in x_ticks: | |
| x_ticks.append(games[-1]) | |
| ax.set_xticks(x_ticks) | |
| ax.set_title( | |
| "Karpov vs. Kasparov First Match Average Centipawn Loss", | |
| loc="left", | |
| fontsize=14, | |
| fontweight="normal", | |
| pad=12, | |
| ) | |
| karp_wins = len(karp_win_x) | |
| kasp_wins = len(kasp_win_x) | |
| ax.text( | |
| 0.03, | |
| 0.985, | |
| f"Filled dots = wins ({karp_wins} Karpov, {kasp_wins} Kasparov)", | |
| transform=ax.transAxes, | |
| fontsize=10, | |
| color=TUFTE_GRAY, | |
| fontfamily="sans-serif", | |
| ha="left", | |
| va="top", | |
| ) | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| fig.subplots_adjust(left=0.10, right=0.96, top=0.90) | |
| fig.savefig(out_path, dpi=150, facecolor=TUFTE_BG) | |
| plt.close(fig) | |
| def main() -> None: | |
| csv_path = Path(sys.argv[1] if len(sys.argv) > 1 else DEFAULT_CSV) | |
| out_path = Path(sys.argv[2] if len(sys.argv) > 2 else DEFAULT_OUT) | |
| pgn_path = csv_path.with_name(DEFAULT_PGN) | |
| rows = load_rows(csv_path) | |
| games = [int(r["Game"]) for r in rows] | |
| karp = [int(r["karp_ACPL"]) for r in rows] | |
| kasp = [int(r["kasp_ACPL"]) for r in rows] | |
| winners = load_game_winners(pgn_path) if pgn_path.exists() else {} | |
| plot_acpl_raw(games, karp, kasp, out_path, winners=winners) | |
| print(f"Wrote {out_path}") | |
| if __name__ == "__main__": | |
| main() |
cavedave
commented
Jun 9, 2026
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment