Answers.Body: <p>The library <a href="https://python-chess.readthedocs.io/en/latest/" rel="nofollow noreferrer">python chess</a> 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.</p>
  <p>I took the <a href="https://theweekinchess.com/chessnews/events/world-rapid-team-championship-2023" rel="nofollow noreferrer">rapid team champ</a> games from the weekinchess and analyze each position.</p>
  <h3>Result</h3>
  <pre><code>num_games: 1296, possible_ep: 146, played_ep: 82, better_epmove_than_played_move: 17 (11.64%)
  </code></pre>
  <p>You can run other pgn file with the following code.</p>
  <h1>Code</h1>
  <pre><code>&quot;&quot;&quot;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
  &quot;&quot;&quot;
  
  
  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 &gt; gscore:
                  better_epmove += 1
  
                  # Save the epd where the ep move is better but not played.
                  epdr = f'{board.epd()} c0 &quot;engine_move: {bm}, engine_score: {bscore}, game_move: {gmove}, game_score: {gscore}, ep_move: {epmove}, ep_score: {epscore}&quot;;'
                  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}%)')
  </code></pre>
  <h2>Sample output</h2>
  <p>World Rapid Team Championship 2023 games.</p>
  <p>Better ep move but not played by the player in the game.</p>
  <pre><code>rnbqk1nr/pp3p1p/3p2p1/2pPp3/2P1P3/2P5/P4PPP/R1BQKBNR w KQkq e6 c0 &quot;engine_move: a2a4, engine_score: 53, game_move: g1e2, game_score: 7, ep_move: d5e6, ep_score: 28&quot;;
  2r1r1k1/5pbp/pp1p1np1/n2Pp2q/3NP3/PP4PP/1B1Q1PB1/R4RK1 w - e6 c0 &quot;engine_move: d5e6, engine_score: 243, game_move: g3g4, game_score: -132, ep_move: d5e6, ep_score: 243&quot;;
  8/1p6/6kp/pPp5/P3P3/8/5KPP/8 w - c6 c0 &quot;engine_move: b5c6, engine_score: 581, game_move: b5b6, game_score: 532, ep_move: b5c6, ep_score: 581&quot;;
  r2r2k1/1ppqbppp/2n1p3/8/pPPPb3/P1Q1BN1P/4BPP1/R2R2K1 b - b3 c0 &quot;engine_move: a4b3, engine_score: 44, game_move: e7f6, game_score: -98, ep_move: a4b3, ep_score: 44&quot;;
  rn1qkb1r/6p1/p2pp3/4p1Pp/4P3/1PN1B3/1PP4P/R2QK1R1 w Qkq h6 c0 &quot;engine_move: g5h6, engine_score: 333, game_move: d1f3, game_score: 296, ep_move: g5h6, ep_score: 333&quot;;
  2rr2k1/1p1b1pbp/nqpp1np1/p2Pp3/2PNP3/PPN3PP/1B3PB1/1R1Q1RK1 w - e6 c0 &quot;engine_move: d5e6, engine_score: 138, game_move: d4e2, game_score: -50, ep_move: d5e6, ep_score: 138&quot;;
  r3k2r/pp3p2/3P4/2pPp3/3q2pp/1R4P1/P1bN1PP1/4QRK1 w kq e6 c0 &quot;engine_move: d5e6, engine_score: 0, game_move: b3e3, game_score: -67, ep_move: d5e6, ep_score: 0&quot;;
  8/1pp1r1p1/p3k2p/P3Pp2/1P1RK2P/1PP3P1/8/8 w - f6 c0 &quot;engine_move: e5f6, engine_score: 165, game_move: e4f4, game_score: 9, ep_move: e5f6, ep_score: 165&quot;;
  3r1nk1/p5p1/1p2p1p1/5pP1/2P2P1P/P1P1RK2/3r4/2Q5 w - f6 c0 &quot;engine_move: g5f6, engine_score: 31, game_move: e3e2, game_score: -57, ep_move: g5f6, ep_score: 31&quot;;
  2r2r2/1b2b1kp/1q2p1p1/p1npPp2/2pN1PP1/PpP1PR2/1P1BQ2P/RB4K1 w - f6 c0 &quot;engine_move: d2e1, engine_score: -101, game_move: g4g5, game_score: -222, ep_move: e5f6, ep_score: -158&quot;;
  2r1r2k/5p2/5Pp1/1p4Pp/4PQ1P/3R4/1q6/3R1K2 w - h6 c0 &quot;engine_move: g5h6, engine_score: -178, game_move: d3d7, game_score: -247, ep_move: g5h6, ep_score: -178&quot;;
  rnbqkb1r/pp3ppp/4pn2/2p5/2PpP3/3P1NP1/PP3PBP/RNBQK2R b KQkq e3 c0 &quot;engine_move: d4e3, engine_score: -43, game_move: b8c6, game_score: -54, ep_move: d4e3, ep_score: -43&quot;;
  r2qr1k1/1p3pbp/p2p1np1/2pPp3/2PnP3/2N1B1P1/PP1Q1PBP/R4RK1 w - c6 c0 &quot;engine_move: d5c6, engine_score: 29, game_move: c3e2, game_score: -41, ep_move: d5c6, ep_score: 29&quot;;
  r1b2rk1/pp2b1pp/2q1p3/3pPp2/2pP3P/2N2P2/1PPQN1P1/R3K2R w KQ f6 c0 &quot;engine_move: h4h5, engine_score: -193, game_move: e1f2, game_score: -174, ep_move: e5f6, ep_score: -133&quot;;
  r1bq1rk1/1pb3pp/p4n2/3nNp2/P1NPpP2/2P5/1P2B1PP/R1BQ1RK1 b - f3 c0 &quot;engine_move: c8e6, engine_score: -128, game_move: c8d7, game_score: -224, ep_move: e4f3, ep_score: -158&quot;;
  1r1r2k1/pq5p/6pQ/4Pp2/3P3R/8/1n4PP/2B2RK1 w - f6 c0 &quot;engine_move: c1b2, engine_score: 433, game_move: c1b2, game_score: 433, ep_move: e5f6, ep_score: 558&quot;;
  3r1bk1/5p2/6p1/1Q4Pp/4P1K1/4RNP1/8/3q4 w - h6 c0 &quot;engine_move: g5h6, engine_score: -26, game_move: g4f4, game_score: -471, ep_move: g5h6, ep_score: -26&quot;;
  </code></pre>
  <h1>Code 2</h1>
  <p>Revised code to calculate if ep move is top engine move, good move (score_rate &gt;= 55%), bad move (score_rate &lt;= 45) and equal move (score_rate &gt; 45% and score_rate &lt; 55%), regardless if this ep move is played or not in the actual game.</p>
  <pre><code>&quot;&quot;&quot;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
  &quot;&quot;&quot;
  
  
  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 &gt; gscore:
                  better_epmove += 1
  
                  # Save the epd where the ep move is better but not played.
                  epdr = f'{board.epd()} c0 &quot;engine_move: {bm}, engine_score: {bscore}, game_move: {gmove}, game_score: {gscore}, ep_move: {epmove}, ep_score: {epscore}&quot;;'
                  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 &gt;= 0.55:
                  good_move += 1
              elif epexp &lt;= 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}%)')
  </code></pre>
  <h3>Algorithm on good, bad, and equal ep move</h3>
  <pre><code>if epmove == bm:
      top_engine_move += 1
  elif epexp &gt;= 0.55:
      good_move += 1
  elif epexp &lt;= 0.45:
      bad_move += 1
  else:
      equal_move += 1
  </code></pre>
  <p>The epexp is the score probability (1, 0) of the ep move according to stockfish 16 at 0.5 second of analysis.</p>
  <h2>sample output</h2>
  <pre><code>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%)
  </code></pre>
  <h2>Sample result from world cup 2023</h2>
  <pre><code>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></pre>
  <h1>Code 3</h1>
  <p>Add ep stats by eco, opening and variation.</p>
  <pre><code>&quot;&quot;&quot;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
  &quot;&quot;&quot;
  
  
  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 &gt; gscore:
                  better_epmove += 1
  
                  # Save the epd where the ep move is better but not played.
                  epdr = f'{board.epd()} c0 &quot;engine_move: {bm}, engine_score: {bscore}, game_move: {gmove}, game_score: {gscore}, ep_move: {epmove}, ep_score: {epscore}&quot;;'
                  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 &gt;= 0.55:
                  good_move += 1
              elif epexp &lt;= 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}')
  </code></pre>
  <h2>Sample output</h2>
  <pre><code>...
  
  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', &quot;variation: King's Indian attack&quot;): 1
  ('A08', 'opening: Reti', &quot;variation: King's Indian attack, French variation&quot;): 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', &quot;opening: Queen's pawn&quot;, 'variation: English defence'): 1
  ('A42', 'opening: Modern defence', 'variation: Averbakh system'): 1
  ('A45', 'opening: Trompovsky attack (Ruth, Opovcensky opening)', 'variation: None'): 1
  ('A46', &quot;opening: Queen's pawn game&quot;, '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', &quot;opening: Queen's pawn game&quot;, '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', &quot;opening: Queen's pawn game&quot;, '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', &quot;variation: King's Indian attack&quot;): 0
  ('A05', 'opening: Reti', &quot;variation: King's Indian attack, Spassky's variation&quot;): 0
  
  ...
  </code></pre>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-09-30T10:59:10.227Z
Answers.Text: 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.
Answers.UserDisplayName: James Martin
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-09-30T13:49:08.317Z
Answers.Text: 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.
Answers.UserDisplayName: ferdy
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-10-01T03:22:27.460Z
Answers.Text: 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.
Answers.UserDisplayName: ferdy
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-10-05T00:59:00.573Z
Answers.Text: @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!
Answers.UserDisplayName: Snowy Oz
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-10-05T03:16:03.910Z
Answers.Text: Added code 3, calculate ep move stats by eco, opening and variation names.
Answers.UserDisplayName: ferdy
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-09-30T01:28:53.437Z
Answers.LastActivityDate: 2023-10-05T03:22:23.993Z
Answers.LastEditDate: 2023-10-05T03:22:23.993Z
Answers.UserDisplayName: ferdy
Answers.Body: <p>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.</p>
  <p>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.</p>
  <p>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</p>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-09-27T19:36:53.180Z
Answers.LastActivityDate: 2023-09-27T19:36:53.180Z
Answers.UserDisplayName: Jan
Answers.Body: <p>I don't think these statistics will work out anything like the way you suggest!</p>
  <p>85% for castling seemed massively high, so I did a quick experiment, looking at the <a href="https://lichess.org/games/blitz" rel="nofollow noreferrer">15 top-rated blitz games</a> in progress on lichess just now. All the players involved are rated 2400+.</p>
  <p>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%.</p>
  <p>So you see that castling is extremely common <em>per game</em> but not particularly common <em>per opportunity</em>.</p>
  <p>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:</p>
  <ol>
  <li>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 ...</li>
  </ol>
  <p>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.</p>
  <p>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%.</p>
  <p>On the other hand, in my small sample, <em>every</em> time a player had the opportunity to castle king-side, they did take it <em>eventually</em>: 27/27.</p>
  <p>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 <em>larger</em> 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 :)</p>
  <p><strong>Edited to add</strong>: 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.</p>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-09-28T09:01:16.137Z
Answers.LastActivityDate: 2023-09-30T11:01:56.447Z
Answers.LastEditDate: 2023-09-30T11:01:56.447Z
Answers.UserDisplayName: James Martin
Answers.Body: <p>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.</p>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2023-09-29T11:36:26.800Z
Answers.LastActivityDate: 2023-09-29T11:36:26.800Z
Answers.UserDisplayName: occipita
Body: <p>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)?</p>
  <p>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.</p>
  <p>Given that, wonder if we can determine a &quot;when EP is available for capture, what is the probability that capture is a weak/strong move at a competitive level&quot;.</p>
  <p>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?)</p>
  <p>Does this kind of analysis even make sense?</p>
  <p>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.</p>
  
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2023-09-27T18:20:43.613Z
Comments.Text: 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?
Comments.UserDisplayName: Philip Roe
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2023-09-28T02:41:11.297Z
Comments.Text: 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?)
Comments.UserDisplayName: Snowy Oz
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2023-10-01T20:29:28.463Z
Comments.Text: OK I now see what you are getting at. Perhaps knowing the statistics would be another thing they can feel proud of?
Comments.UserDisplayName: Philip Roe
Tags: <statistics><en-passant>
Title: En passant capture - statistically advantageous?
ThreadText: # En passant capture - statistically advantageous?
  
  (Asked by: username_0 on 2023-09-27)
  
  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.
  
  **Tags:** statistics, en-passant
  
  > **Comments:**
  > **username_1**: 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?  
  > **username_0**: 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?)  
  > **username_1**: OK I now see what you are getting at. Perhaps knowing the statistics would be another thing they can feel proud of?  
  
  ---
  
  ## 4 answers
  
  ### Answer by username_2 on 2023-09-30. Score: 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
  
  ...
  
  > **Comments:**
  > **username_3**: 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.  
  > **username_2**: 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.  
  > **username_2**: 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.  
  > **username_0**: @username_2 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!  
  > **username_2**: Added code 3, calculate ep move stats by eco, opening and variation names.  
  
  ---
  
  ### Answer by username_4 on 2023-09-27. Score: 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
  
  
  ---
  
  ### Answer by username_3 on 2023-09-28. Score: 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](https://lichess.org/games/blitz) 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 username_2's answer he gets a statistic of 82/146 en passant moves played per opportunity, or roughly 56%, in a sample of 1296 games.
  
  
  ---
  
  ### Answer by username_5 on 2023-09-29. Score: 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.
  
  
  ---
  
  