3

If anyone is doing chess score analysis (on say, last 10 years of competitive games), I wonder if it's identifiable how many times en passant (EP) capture could have been played vs how many times it was played (and also ignored)?

I guess without analyzing the position post the EP capture/non-capture move, we can assume that it was weaker move if the (assume again, ranked) player didn't proceed with the capture.

Given that, wonder if we can determine a "when EP is available for capture, what is the probability that capture is a weak/strong move at a competitive level".

Eg, if this was 5 in 100, then we can more or less say that EP capture is the strongest move in 5% of cases. (I would say, castling, for example, is probably like 85%+ when possible?)

Does this kind of analysis even make sense?

It's maybe a guide for beginner players to say, if you get presented with EP capture, it's likely only the best move 5% of the time. etc.

3
  • This is kind-of interesting, but as a beginner you are clutching at straws here. You will derive no benefit from knowing this. Perhaps ask rather, WHEN it is the best move, WHY is it the best move?
    – Philip Roe
    Sep 27, 2023 at 18:20
  • oh I'm not a beginner at chess, but my question wasn't really about chess per se either. I wanted to study a concept like en passant to map other cognitive concepts that "people prefer fancy solutions that don't solve real problems". EP seems a good fit for that model as most junior players would almost NEVER give up a chance to play an EP capture (anecdotally). (maybe a dunning-kruger sort of bias?)
    – Snowy Oz
    Sep 28, 2023 at 2:41
  • 1
    OK I now see what you are getting at. Perhaps knowing the statistics would be another thing they can feel proud of?
    – Philip Roe
    Oct 1, 2023 at 20:29

4 Answers 4

6

The library python chess can determine if a position has a legal ep move. I use this library to calculate if an ep move is better than the played move in the game using stockfish 16 engine at 1 sec/pos.

I took the rapid team champ games from the weekinchess and analyze each position.

Result

num_games: 1296, possible_ep: 146, played_ep: 82, better_epmove_than_played_move: 17 (11.64%)

You can run other pgn file with the following code.

Code

"""Get stats on unplayed but better ep move.

Read every position in the game from pgn file. Check
if position has a legal ep move, the played move in the game
and if ep move is better than the played move by analyzing
the position with stockfish engine.

requirements:
    pip install chess
"""


import chess
import chess.pgn
import chess.engine


fn = 'wrapteam23.pgn'
gcnt = 0

engine = chess.engine.SimpleEngine.popen_uci(
    r'F:\Chess\Engines\stockfish\sf16\sf16pop.exe'
)

engine.configure({'Hash': 256})

possible_ep, played_ep, better_epmove = 0, 0, 0
at = 1.0  # analysis time in seconds
epds = []

with open(fn) as pgn:
    while True:
        game = chess.pgn.read_game(pgn)
        if game is None:
            break

        gcnt += 1
        print(f'game {gcnt}')

        for node in game.mainline():
            board = node.parent.board()
            gmove = node.move  # the actual move played in the game

            # Check if there is legal ep capture.
            if not board.has_legal_en_passant():
                continue

            possible_ep += 1
            ep_square = board.ep_square

            # Get the best move and score of the engine from the current position.
            info = engine.analyse(board, chess.engine.Limit(time=at))
            bm = info['pv'][0]
            bscore = info['score'].relative.score(mate_score=32000)

            # Determine the ep move score.
            if bm.to_square == ep_square:
                epmove = bm
                epscore = bscore
            else:
                for mv in board.legal_moves:
                    to_sq = mv.to_square
                    if to_sq == ep_square:
                        tboard = board.copy()                    

                        # Push this move and analyze the position with the engine.
                        # Negate the score because we push it.
                        tboard.push(mv)
                        info = engine.analyse(tboard, chess.engine.Limit(time=at))
                        epscore = -info['score'].relative.score(mate_score=32000)
                        epmove = mv
                        break

            # Determine the game move score.
            if bm == gmove:
                gscore = bscore
            elif gmove == epmove:
                gscore = epscore
            else:
                tboard = board.copy()  
                tboard.push(gmove)
                info = engine.analyse(tboard, chess.engine.Limit(time=at))
                gscore = -info['score'].relative.score(mate_score=32000)

            # Determine if epmove is played or epmove is better than gmove.
            if gmove == epmove:
                played_ep += 1
            elif epscore > gscore:
                better_epmove += 1

                # Save the epd where the ep move is better but not played.
                epdr = f'{board.epd()} c0 "engine_move: {bm}, engine_score: {bscore}, game_move: {gmove}, game_score: {gscore}, ep_move: {epmove}, ep_score: {epscore}";'
                epds.append(epdr)

engine.quit()

print('Better ep move but not played:')
for e in epds:
    print(e)

print(f'num_games: {gcnt}, possible_ep: {possible_ep}, played_ep: {played_ep}, better_epmove_than_played_move: {better_epmove} ({100*better_epmove / possible_ep:0.2f}%)')

Sample output

World Rapid Team Championship 2023 games.

Better ep move but not played by the player in the game.

rnbqk1nr/pp3p1p/3p2p1/2pPp3/2P1P3/2P5/P4PPP/R1BQKBNR w KQkq e6 c0 "engine_move: a2a4, engine_score: 53, game_move: g1e2, game_score: 7, ep_move: d5e6, ep_score: 28";
2r1r1k1/5pbp/pp1p1np1/n2Pp2q/3NP3/PP4PP/1B1Q1PB1/R4RK1 w - e6 c0 "engine_move: d5e6, engine_score: 243, game_move: g3g4, game_score: -132, ep_move: d5e6, ep_score: 243";
8/1p6/6kp/pPp5/P3P3/8/5KPP/8 w - c6 c0 "engine_move: b5c6, engine_score: 581, game_move: b5b6, game_score: 532, ep_move: b5c6, ep_score: 581";
r2r2k1/1ppqbppp/2n1p3/8/pPPPb3/P1Q1BN1P/4BPP1/R2R2K1 b - b3 c0 "engine_move: a4b3, engine_score: 44, game_move: e7f6, game_score: -98, ep_move: a4b3, ep_score: 44";
rn1qkb1r/6p1/p2pp3/4p1Pp/4P3/1PN1B3/1PP4P/R2QK1R1 w Qkq h6 c0 "engine_move: g5h6, engine_score: 333, game_move: d1f3, game_score: 296, ep_move: g5h6, ep_score: 333";
2rr2k1/1p1b1pbp/nqpp1np1/p2Pp3/2PNP3/PPN3PP/1B3PB1/1R1Q1RK1 w - e6 c0 "engine_move: d5e6, engine_score: 138, game_move: d4e2, game_score: -50, ep_move: d5e6, ep_score: 138";
r3k2r/pp3p2/3P4/2pPp3/3q2pp/1R4P1/P1bN1PP1/4QRK1 w kq e6 c0 "engine_move: d5e6, engine_score: 0, game_move: b3e3, game_score: -67, ep_move: d5e6, ep_score: 0";
8/1pp1r1p1/p3k2p/P3Pp2/1P1RK2P/1PP3P1/8/8 w - f6 c0 "engine_move: e5f6, engine_score: 165, game_move: e4f4, game_score: 9, ep_move: e5f6, ep_score: 165";
3r1nk1/p5p1/1p2p1p1/5pP1/2P2P1P/P1P1RK2/3r4/2Q5 w - f6 c0 "engine_move: g5f6, engine_score: 31, game_move: e3e2, game_score: -57, ep_move: g5f6, ep_score: 31";
2r2r2/1b2b1kp/1q2p1p1/p1npPp2/2pN1PP1/PpP1PR2/1P1BQ2P/RB4K1 w - f6 c0 "engine_move: d2e1, engine_score: -101, game_move: g4g5, game_score: -222, ep_move: e5f6, ep_score: -158";
2r1r2k/5p2/5Pp1/1p4Pp/4PQ1P/3R4/1q6/3R1K2 w - h6 c0 "engine_move: g5h6, engine_score: -178, game_move: d3d7, game_score: -247, ep_move: g5h6, ep_score: -178";
rnbqkb1r/pp3ppp/4pn2/2p5/2PpP3/3P1NP1/PP3PBP/RNBQK2R b KQkq e3 c0 "engine_move: d4e3, engine_score: -43, game_move: b8c6, game_score: -54, ep_move: d4e3, ep_score: -43";
r2qr1k1/1p3pbp/p2p1np1/2pPp3/2PnP3/2N1B1P1/PP1Q1PBP/R4RK1 w - c6 c0 "engine_move: d5c6, engine_score: 29, game_move: c3e2, game_score: -41, ep_move: d5c6, ep_score: 29";
r1b2rk1/pp2b1pp/2q1p3/3pPp2/2pP3P/2N2P2/1PPQN1P1/R3K2R w KQ f6 c0 "engine_move: h4h5, engine_score: -193, game_move: e1f2, game_score: -174, ep_move: e5f6, ep_score: -133";
r1bq1rk1/1pb3pp/p4n2/3nNp2/P1NPpP2/2P5/1P2B1PP/R1BQ1RK1 b - f3 c0 "engine_move: c8e6, engine_score: -128, game_move: c8d7, game_score: -224, ep_move: e4f3, ep_score: -158";
1r1r2k1/pq5p/6pQ/4Pp2/3P3R/8/1n4PP/2B2RK1 w - f6 c0 "engine_move: c1b2, engine_score: 433, game_move: c1b2, game_score: 433, ep_move: e5f6, ep_score: 558";
3r1bk1/5p2/6p1/1Q4Pp/4P1K1/4RNP1/8/3q4 w - h6 c0 "engine_move: g5h6, engine_score: -26, game_move: g4f4, game_score: -471, ep_move: g5h6, ep_score: -26";

Code 2

Revised code to calculate if ep move is top engine move, good move (score_rate >= 55%), bad move (score_rate <= 45) and equal move (score_rate > 45% and score_rate < 55%), regardless if this ep move is played or not in the actual game.

"""Get stats on unplayed but better ep move.

Read every positions in the game from pgn file. Check
if position has a legal ep move, the played move in the game
and if ep move is better than the played move by analyzing
the position with stockfish engine

requirements:
    pip install chess
"""


import chess
import chess.pgn
import chess.engine
from chess.engine import Cp


fn = 'wrapteam23.pgn'
gcnt = 0

# Modify engine here.
engine = chess.engine.SimpleEngine.popen_uci(
    r'F:\Chess\Engines\stockfish\sf16\sf16pop.exe'
)

# modify this if you use other stockfish version
WDL_MODEL = 'sf16'

engine.configure({'Hash': 256})
engine_id = engine.id['name']

possible_ep, played_ep, better_epmove = 0, 0, 0
top_engine_move, good_move, equal_move, bad_move = 0, 0, 0, 0
at = 0.5  # 1.0  # analysis time in seconds
epds = []

with open(fn) as pgn:
    while True:
        game = chess.pgn.read_game(pgn)
        if game is None:
            break

        gcnt += 1
        print(f'game {gcnt}')

        for node in game.mainline():
            board = node.parent.board()
            gmove = node.move  # the actual move played in the game

            # Check if there is legal ep capture.
            if not board.has_legal_en_passant():
                continue

            possible_ep += 1
            ep_square = board.ep_square

            # Get the best move and score of the engine from the current position.
            info = engine.analyse(board, chess.engine.Limit(time=at))
            bm = info['pv'][0]
            bscore = info['score'].relative.score(mate_score=32000)

            # Determine the ep move score.
            if bm.to_square == ep_square:
                epmove = bm
                epscore = bscore
            else:
                for mv in board.legal_moves:
                    to_sq = mv.to_square
                    if to_sq == ep_square:
                        tboard = board.copy()                    

                        # Push this move and analyze the position with the engine.
                        # Negate the score because we push it.
                        tboard.push(mv)
                        info = engine.analyse(tboard, chess.engine.Limit(time=at))
                        epscore = -info['score'].relative.score(mate_score=32000)
                        epmove = mv
                        epexp = Cp(epscore).wdl(model=WDL_MODEL, ply=board.ply()).expectation()
                        break

            # Determine the game move score.
            if bm == gmove:
                gscore = bscore
            elif gmove == epmove:
                gscore = epscore
            else:
                tboard = board.copy()  
                tboard.push(gmove)
                info = engine.analyse(tboard, chess.engine.Limit(time=at))
                gscore = -info['score'].relative.score(mate_score=32000)

            # Determine if epmove is played or epmove is better than gmove.
            if gmove == epmove:
                played_ep += 1
            elif epscore > gscore:
                better_epmove += 1

                # Save the epd where the ep move is better but not played.
                epdr = f'{board.epd()} c0 "engine_move: {bm}, engine_score: {bscore}, game_move: {gmove}, game_score: {gscore}, ep_move: {epmove}, ep_score: {epscore}";'
                epds.append(epdr)

            # Determine if ep move is strong or weak or close to equal.
            # Strong ep move:
            # 1. Top engine move.
            # 2. If not top, win percentage must be 55 or more percent.
            # weak ep move:
            # 1. win percentage is 45 and below percent
            # close to equal ep move
            # 1. win percentage is within [46 to 54] percent
            if epmove == bm:
                top_engine_move += 1
            elif epexp >= 0.55:
                good_move += 1
            elif epexp <= 0.45:
                bad_move += 1
            else:
                equal_move += 1

engine.quit()

# Summaries
print('Better ep move but not played:')
for e in epds:
    print(e)
print()

print(f'analysis engine: {engine_id}')
print(f'position analysis time (sec): {at}')
print(f'pgn file: {fn}')
print()

print(f'num games: {gcnt}')
print(f'possible ep move: {possible_ep}')
print(f'played ep move: {played_ep} ({100*played_ep/possible_ep:0.2f}%)')
print(f'better ep move than played move: {better_epmove} ({100*better_epmove / possible_ep:0.2f}%)')
print()

print(f'top engine ep move: {top_engine_move} ({100*top_engine_move/possible_ep:0.2f}%)')
print(f'good ep move: {good_move} ({100*good_move/possible_ep:0.2f}%)')
print(f'equal ep move: {equal_move} ({100*equal_move/possible_ep:0.2f}%)')
print(f'bad ep move: {bad_move} ({100*bad_move/possible_ep:0.2f}%)')

Algorithm on good, bad, and equal ep move

if epmove == bm:
    top_engine_move += 1
elif epexp >= 0.55:
    good_move += 1
elif epexp <= 0.45:
    bad_move += 1
else:
    equal_move += 1

The epexp is the score probability (1, 0) of the ep move according to stockfish 16 at 0.5 second of analysis.

sample output

analysis engine: Stockfish 16
position analysis time (sec): 0.5
pgn file: wrapteam23.pgn

num games: 1296
possible ep move: 146
played ep move: 81 (55.48%)
better ep move than played move: 18 (12.33%)

top engine ep move: 79 (54.11%)
good ep move: 10 (6.85%)
equal ep move: 19 (13.01%)
bad ep move: 38 (26.03%)

Sample result from world cup 2023

analysis engine: Stockfish 16
position analysis time (sec): 0.5
pgn file: wcup23.pgn

num games: 677
possible ep move: 92
played ep move: 47 (51.09%)
better ep move than played move: 7 (7.61%)

top engine ep move: 39 (42.39%)
good ep move: 6 (6.52%)
equal ep move: 18 (19.57%)
bad ep move: 29 (31.52%)

Code 3

Add ep stats by eco, opening and variation.

"""Get stats on unplayed but better ep move.

Read every positions in the game from pgn file. Check
if position has a legal ep move, the played move in the game
and if ep move is better than the played move by analyzing
the position with stockfish engine

requirements:
    pip install chess
"""


import chess
import chess.pgn
import chess.engine
from chess.engine import Cp


fn = 'wcup23.pgn'  # 'wrapteam23.pgn'
gcnt = 0

# Modify engine here.
engine = chess.engine.SimpleEngine.popen_uci(
    r'F:\Chess\Engines\stockfish\sf16\sf16pop.exe'
)

# modify this if you use other stockfish version
WDL_MODEL = 'sf16'

engine.configure({'Hash': 256})
engine_id = engine.id['name']

possible_ep, played_ep, better_epmove = 0, 0, 0
top_engine_move, good_move, equal_move, bad_move = 0, 0, 0, 0
at = 0.5  # 1.0  # analysis time in seconds
epds = []
eco_ep_data = {}

with open(fn) as pgn:
    while True:
        game = chess.pgn.read_game(pgn)
        if game is None:
            break

        gcnt += 1
        print(f'game {gcnt}')

        eco = game.headers.get('ECO', None)
        opening = game.headers.get('Opening', None)
        variation = game.headers.get('Variation', None)
        eco_cnt = 0

        for node in game.mainline():
            board = node.parent.board()
            gmove = node.move  # the actual move played in the game

            # Check if there is legal ep capture.
            if not board.has_legal_en_passant():
                continue

            possible_ep += 1
            ep_square = board.ep_square

            eco_cnt += 1

            # Get the best move and score of the engine from the current position.
            info = engine.analyse(board, chess.engine.Limit(time=at))
            bm = info['pv'][0]
            bscore = info['score'].relative.score(mate_score=32000)

            # Determine the ep move score.
            if bm.to_square == ep_square:
                epmove = bm
                epscore = bscore
            else:
                for mv in board.legal_moves:
                    to_sq = mv.to_square
                    if to_sq == ep_square:
                        tboard = board.copy()                    

                        # Push this move and analyze the position with the engine.
                        # Negate the score because we push it.
                        tboard.push(mv)
                        info = engine.analyse(tboard, chess.engine.Limit(time=at))
                        epscore = -info['score'].relative.score(mate_score=32000)
                        epmove = mv
                        epexp = Cp(epscore).wdl(model=WDL_MODEL, ply=board.ply()).expectation()
                        break

            # Determine the game move score.
            if bm == gmove:
                gscore = bscore
            elif gmove == epmove:
                gscore = epscore
            else:
                tboard = board.copy()  
                tboard.push(gmove)
                info = engine.analyse(tboard, chess.engine.Limit(time=at))
                gscore = -info['score'].relative.score(mate_score=32000)

            # Determine if epmove is played or epmove is better than gmove.
            if gmove == epmove:
                played_ep += 1
            elif epscore > gscore:
                better_epmove += 1

                # Save the epd where the ep move is better but not played.
                epdr = f'{board.epd()} c0 "engine_move: {bm}, engine_score: {bscore}, game_move: {gmove}, game_score: {gscore}, ep_move: {epmove}, ep_score: {epscore}";'
                epds.append(epdr)

            # Determine if ep move is strong or weak or close to equal.
            # Strong ep move:
            # 1. Top engine move.
            # 2. If not top, win percentage must be 55 or more percent.
            # weak ep move:
            # 1. win percentage is 45 and below percent
            # close to equal ep move
            # 1. win percentage is within [46 to 54] percent
            if epmove == bm:
                top_engine_move += 1
            elif epexp >= 0.55:
                good_move += 1
            elif epexp <= 0.45:
                bad_move += 1
            else:
                equal_move += 1

        # Save ep counts by ECO opening.
        try:
            eco_ep_data[(eco, f'opening: {opening}', f'variation: {variation}')] += eco_cnt
        except KeyError:
            eco_ep_data.update({(eco, f'opening: {opening}', f'variation: {variation}'): eco_cnt})

engine.quit()

# Summaries

print(f'analysis engine: {engine_id}')
print(f'position analysis time (sec): {at}')
print(f'pgn file: {fn}')
print()


print('Better ep move but not played:')
for e in epds:
    print(e)
print()

print(f'num games: {gcnt}')
print(f'possible ep move: {possible_ep}')
print(f'played ep move: {played_ep} ({100*played_ep/possible_ep:0.2f}%)')
print(f'better ep move than played move: {better_epmove} ({100*better_epmove / possible_ep:0.2f}%)')
print()

print(f'top engine ep move: {top_engine_move} ({100*top_engine_move/possible_ep:0.2f}%)')
print(f'good ep move: {good_move} ({100*good_move/possible_ep:0.2f}%)')
print(f'equal ep move: {equal_move} ({100*equal_move/possible_ep:0.2f}%)')
print(f'bad ep move: {bad_move} ({100*bad_move/possible_ep:0.2f}%)')
print()

# ep move possibility by ECO.
eco_ep_data = dict(sorted(eco_ep_data.items()))
eco_ep_data = dict(sorted(eco_ep_data.items(), key=lambda item: item[1], reverse=True))
for k, v in eco_ep_data.items():
    print(f'{k}: {v}')

Sample output

...

top engine ep move: 42 (45.65%)
good ep move: 8 (8.70%)
equal ep move: 16 (17.39%)
bad ep move: 26 (28.26%)

('C43', 'opening: Petrov', 'variation: modern attack, Symmetrical variation'): 4
('C53', 'opening: Giuoco Piano', 'variation: None'): 4
('A13', 'opening: English opening', 'variation: None'): 3
('A20', 'opening: English opening', 'variation: None'): 3
('B12', 'opening: Caro-Kann', 'variation: advance variation'): 3
('B40', 'opening: Sicilian', 'variation: Anderssen variation'): 3
('B45', 'opening: Sicilian', 'variation: Taimanov variation'): 3
('A22', 'opening: English', 'variation: Bremen, Smyslov system'): 2
('A22', 'opening: English opening', 'variation: None'): 2
('B33', 'opening: Sicilian', 'variation: Pelikan (Lasker/Sveshnikov) variation'): 2
('B50', 'opening: Sicilian', 'variation: None'): 2
('B51', 'opening: Sicilian', 'variation: Canal-Sokolsky (Nimzovich-Rossolimo, Moscow) attack'): 2
('C02', 'opening: French', 'variation: advance, Paulsen attack'): 2
('C44', 'opening: Scotch gambit', 'variation: Dubois-Reti defence'): 2
('C67', 'opening: Ruy Lopez', 'variation: Berlin defence, open variation'): 2
('D35', 'opening: QGD', 'variation: exchange, positional line'): 2
('D37', 'opening: QGD', 'variation: 4.Nf3'): 2
('D43', 'opening: QGD semi-Slav', 'variation: None'): 2
('D80', 'opening: Gruenfeld', 'variation: Stockholm variation'): 2
('A07', 'opening: Reti', "variation: King's Indian attack"): 1
('A08', 'opening: Reti', "variation: King's Indian attack, French variation"): 1
('A09', 'opening: Reti opening', 'variation: None'): 1
('A13', 'opening: English', 'variation: Neo-Catalan'): 1
('A13', 'opening: English', 'variation: Neo-Catalan accepted'): 1
('A13', 'opening: English opening', 'variation: Agincourt variation'): 1
('A28', 'opening: English', 'variation: four knights, Stean variation'): 1
('A29', 'opening: English', 'variation: four knights, kingside fianchetto'): 1
('A40', "opening: Queen's pawn", 'variation: English defence'): 1
('A42', 'opening: Modern defence', 'variation: Averbakh system'): 1
('A45', 'opening: Trompovsky attack (Ruth, Opovcensky opening)', 'variation: None'): 1
('A46', "opening: Queen's pawn game", 'variation: None'): 1
('A56', 'opening: Benoni defence', 'variation: None'): 1
('A62', 'opening: Benoni', 'variation: fianchetto variation'): 1
('A81', 'opening: Dutch defence', 'variation: None'): 1
('B11', 'opening: Caro-Kann', 'variation: two knights, 3...Bg4'): 1
('B23', 'opening: Sicilian', 'variation: closed'): 1
('B30', 'opening: Sicilian', 'variation: Nimzovich-Rossolimo attack (without ...d6)'): 1
('B31', 'opening: Sicilian', 'variation: Nimzovich-Rossolimo attack (with ...g6, without ...d6)'): 1
('B33', 'opening: Sicilian defence', 'variation: None'): 1
('B39', 'opening: Sicilian', 'variation: accelerated fianchetto, Breyer variation'): 1
('B52', 'opening: Sicilian', 'variation: Canal-Sokolsky attack, 3...Bd7'): 1
('B67', 'opening: Sicilian', 'variation: Richter-Rauzer, Rauzer attack, 7...a6 defence, 8...Bd7'): 1
('B90', 'opening: Sicilian', 'variation: Najdorf'): 1
('B90', 'opening: Sicilian', 'variation: Najdorf, Byrne (English) attack'): 1
('C07', 'opening: French', 'variation: Tarrasch, open variation'): 1
('C22', 'opening: Centre game', 'variation: Berger variation'): 1
('C42', 'opening: Petrov', 'variation: classical attack'): 1
('C44', 'opening: Scotch gambit', 'variation: None'): 1
('C46', 'opening: Four knights game', 'variation: None'): 1
('C50', 'opening: Giuoco Pianissimo', 'variation: None'): 1
('C65', 'opening: Ruy Lopez', 'variation: Berlin defence, Mortimer variation'): 1
('C72', 'opening: Ruy Lopez', 'variation: modern Steinitz defence, 5.O-O'): 1
('C80', 'opening: Ruy Lopez', 'variation: open, 8...Be6'): 1
('C80', 'opening: Ruy Lopez', 'variation: open, Bernstein variation'): 1
('C85', 'opening: Ruy Lopez', 'variation: Exchange variation doubly deferred (DERLD)'): 1
('D02', "opening: Queen's pawn game", 'variation: None'): 1
('D10', 'opening: QGD Slav defence', 'variation: exchange variation'): 1
('D31', 'opening: QGD', 'variation: semi-Slav, Marshall gambit'): 1
('D35', 'opening: QGD', 'variation: exchange variation'): 1
('D47', 'opening: QGD semi-Slav', 'variation: Meran variation'): 1
('D78', 'opening: Neo-Gruenfeld, 6.O-O c6', 'variation: None'): 1
('E04', 'opening: Catalan', 'variation: open, 5.Nf3'): 1
('E10', "opening: Queen's pawn game", 'variation: None'): 1
('E29', 'opening: Nimzo-Indian', 'variation: Saemisch, main line'): 1
('A01', 'opening: Nimzovich-Larsen attack', 'variation: classical variation'): 0
('A01', 'opening: Nimzovich-Larsen attack', 'variation: modern variation'): 0
('A04', 'opening: Reti opening', 'variation: None'): 0
('A05', 'opening: Reti', "variation: King's Indian attack"): 0
('A05', 'opening: Reti', "variation: King's Indian attack, Spassky's variation"): 0

...
5
  • Great! So in that sample the en passants played per opportunity comes out at 82/146, roughly 56% - considerbly higher than the corresponding statistic for castling. Sep 30, 2023 at 10:59
  • Yes for 82/146, but I have not run for castling. Also, I have not checked if the ep move played in the game is best or not, although the code can be revised to addressed that.
    – ferdy
    Sep 30, 2023 at 13:49
  • I modify my answer (code 2) to include stats if an ep move is a top engine move, good, bad or equal regardless if this move is played or not.
    – ferdy
    Oct 1, 2023 at 3:22
  • @ferdy amazing analysis! 92/677 is 13.58% of games that had a possible ep move - doesn't that seem rather high? Thanks for the code, I will try to play around with it too!
    – Snowy Oz
    Oct 5, 2023 at 0:59
  • Added code 3, calculate ep move stats by eco, opening and variation names.
    – ferdy
    Oct 5, 2023 at 3:16
4

We can of course grab the statistics for a period of time, compare the eval difference between performing EP and missing it and get some % in which it's best move.

The problem is, this wouldn't be by any means representative and informative to newer players because it still comes down to the fact that each position has it's own best move which is unique and bases on a vast number of factors.

I do believe that the best advice for beginners regarding EP is to simply remember it's possible and to calculate it as the first option when the opponent moves the pawn from the starting position next to your pawn

0
3

I don't think these statistics will work out anything like the way you suggest!

85% for castling seemed massively high, so I did a quick experiment, looking at the 15 top-rated blitz games in progress on lichess just now. All the players involved are rated 2400+.

By the end of the games, there had been (for white and black together) a total of 75 opportunities to castle king-side. In total there were 27 king-side castling moves. So on this small sample, you get something around 36%.

So you see that castling is extremely common per game but not particularly common per opportunity.

There are just too many cases where it's possible to castle but there's no hurry to do so. For example, one of the games followed a fairly normal-looking Spanish line:

  1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. d3 b5 6. Bb3 Bc5 7. O-O h6 8. c3 d6 9. a4 Rb8 10. d4 Bb6 11. Bd5 Nxd5 12. exd5 Ne7 13. axb5 axb5 14. Na3 O-O ...

White castled at the 4th opportunity and black at the 8th. Obviously it would be bad for white to castle at the 1st opportunity - it loses a bishop.

Suppose you instead look at the proportion of times when a player castled at their first opportunity, as a proportion of the games that they have an opportunity to do so. In my small sample, even that larger statistic was 13/27, less than 50%.

On the other hand, in my small sample, every time a player had the opportunity to castle king-side, they did take it eventually: 27/27.

The en passant statistic would be harder to get a quick estimate of because the situation is so much rarer. Maybe someone with a database and some coding skills can oblige. But I would expect that statistic to be rather larger than the castling one. Quite often, when en passant is possible, it's a good move. And when it's not, the opportunity goes away immediately - you don't typically get 8 opportunities in a game as Black had for king-side castling in the game above. What makes you think that en passant moves per opportunity is in the region of 5%? Picking a number out of the air, I'm going to go with 50% (in high-level games). If someone wants to put in the time to do a calculation, it would be fun to find out how far off that flying guess is :)

Edited to add: in ferdy's answer he gets a statistic of 82/146 en passant moves played per opportunity, or roughly 56%, in a sample of 1296 games.

2

One issue I see with this kind of analysis is that it probably depends very highly on the experience of your opponent: playing against an inexperienced opponent, when an en passant move is available, it is quite likely that they didn't consider the possibility, which makes it more likely to be the best move. Playing against an experienced player, this is unlikely to be true. So to get a meaningful result you'd need to look at a wider variety of games than just high-level competitions, but with lower-level games your signal is likely to be lost in the noise produced by the frequent blunders made by less experienced players.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.