Skip to content

Instantly share code, notes, and snippets.

@cavedave
Last active June 9, 2026 19:41
Show Gist options
  • Select an option

  • Save cavedave/7ef846bf5c47cd0af1871b9a3414229f to your computer and use it in GitHub Desktop.

Select an option

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
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
#!/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

cavedave commented Jun 9, 2026

Copy link
Copy Markdown
Author
acplraw

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment