import argparse
import csv
import math
import os
from collections import Counter
from typing import List

import dataclasses
import numpy as np
from numpy.ma import exp
from scipy.optimize import minimize\_scalar
from scipy.special import expit

PARENT\_DIR = os.path.dirname(os.path.realpath(\_\_file\_\_))

HISTORICAL\_SEED\_WIN\_RATES = {
 (1, 16): 1.000,
 (2, 15): 0.917,
 (3, 14): 0.833,
 (4, 13): 0.750,
 (5, 12): 0.667,
 (6, 11): 0.583,
 (7, 10): 0.583,
 (8, 9): 0.500,
}
@dataclasses.dataclass
class Team:
 team\_name: str
 team\_seed: int
 ken\_pom\_score: float

 @classmethod
 def extract\_teams(cls, file\_path: str):
 with open(os.path.join(PARENT\_DIR, file\_path), "r", newline="") as csvfile:
 return [cls(
 team\_name=row["team"],
 team\_seed=int(row["seed"]),
 ken\_pom\_score=float(row["score"])
 ) for row in (csv.DictReader(csvfile))]
class Tournament:

 def \_\_init\_\_(self, team\_metrics: List[Team]):
 self.team\_metrics: List[Team] = team\_metrics
 self.team\_names = [team.team\_name for team in team\_metrics]
 self.teams = {i: team.team\_name for i, team in enumerate(team\_metrics)}
 self.k = self.find\_best\_k()
 self.adj\_matrix = self.calculate\_adj\_matrix()

 num\_teams = len(self.teams)
 self.num\_rounds = int(math.log2(num\_teams))

 self.round\_winners = {0: list(self.teams.keys())}
 self.round\_win\_counts = {}
 # Construct the bracket
 num\_matches = num\_teams // 2
 self.bracket = {}
 for round\_num in range(1, self.num\_rounds + 1):
 self.bracket[round\_num] = list(range(num\_matches))
 num\_matches //= 2

 # Used for printing results
 self.round\_names = {
 1: "Round of 64",
 2: "Round of 32",
 3: "Sweet Sixteen",
 4: "Elite Eight",
 5: "Final Four",
 6: "Championship",
 }
 self.round\_spacing = {
 1: 2,
 2: 4,
 3: 8,
 4: 16,
 5: 32,
 6: 64,
 }

 def get\_opponent(self, round\_num, team\_index):
 if round\_num == 0:
 opponent\_index = team\_index + 1 if team\_index % 2 == 0 else team\_index - 1
 else:
 previous\_round\_winners = self.round\_winners[round\_num - 1]
 # print(f"Previous round winners: {previous\_round\_winners}") # Debugging print
 team\_position = previous\_round\_winners.index(team\_index)
 opponent\_index = previous\_round\_winners[team\_position + 1 if team\_position % 2 == 0 else team\_position - 1]

 # print(f"Team index: {team\_index}, Opponent index: {opponent\_index}") # Debugging print
 return opponent\_index

 def calculate\_adj\_matrix(self):
 num\_teams = len(self.team\_metrics)
 adj\_matrix = np.zeros((num\_teams, num\_teams))

 for i, team\_i in enumerate(self.team\_metrics):
 for j, team\_j in enumerate(self.team\_metrics):
 if i != j:
 p\_win = self.calculate\_win\_probability(team\_i, team\_j)
 adj\_matrix[i, j] = p\_win
 adj\_matrix[j, i] = 1 - p\_win

 return adj\_matrix

 def calculate\_win\_probability(self, team\_i: Team, team\_j: Team):
 seed\_diff = team\_j.team\_seed - team\_i.team\_seed
 ken\_pom\_diff = team\_i.ken\_pom\_score - team\_j.ken\_pom\_score
 return expit(self.k \* (ken\_pom\_diff + seed\_diff))

 def play\_single\_round(self, remaining\_teams):
 winners = []
 for i in range(0, len(remaining\_teams), 2):
 team\_i = remaining\_teams[i]
 team\_j = remaining\_teams[i + 1]

 p\_win\_i = self.adj\_matrix[team\_i, team\_j]
 win\_i = np.random.rand() < p\_win\_i
 winning\_team\_index = team\_i if win\_i else team\_j
 winners.append(winning\_team\_index)

 return winners

 def get\_remaining\_teams(self, round\_num):
 if round\_num == 0:
 return list(range(len(self.teams)))

 remaining\_teams = []
 for i, team in enumerate(self.teams):
 if self.round\_winners[round\_num - 1].count(team) > 0:
 remaining\_teams.append(i)

 return remaining\_teams

 def simulate\_round\_n(self, round\_num, num\_simulations):
 round\_win\_counts = Counter()

 for \_ in range(num\_simulations):
 remaining\_teams = self.get\_remaining\_teams(round\_num)
 winners = self.play\_single\_round(remaining\_teams)
 for winner in winners:
 round\_win\_counts[self.teams[winner]] += 1

 # Update the round\_winners and round\_win\_counts
 sorted\_teams = sorted(round\_win\_counts.items(), key=lambda x: x[1], reverse=True)
 self.round\_winners[round\_num] = [list(self.teams.keys())[list(self.teams.values()).index(team)] for team, \_ in
 sorted\_teams]
 self.round\_win\_counts[round\_num] = round\_win\_counts

 return round\_win\_counts

 def print\_round\_results(self, round\_num, round\_win\_counts, num\_simulations):
 print(f"{self.round\_names[round\_num]} results:")
 for i, (team\_name, count) in enumerate(round\_win\_counts.most\_common()):
 team\_index = list(self.teams.values()).index(team\_name)
 team = self.teams[team\_index]
 opponent\_index = self.get\_opponent(round\_num, team\_index)
 opponent = self.teams[opponent\_index]
 win\_percentage = (count / num\_simulations) \* 100

 spaces = self.round\_spacing[round\_num] - len(str(i + 1))
 print(f" {i + 1}.{' ' \* spaces}{team} over {opponent} with {win\_percentage:.2f}%")
 print()

 def run\_all\_rounds(self, num\_simulations):
 for round\_num in range(1, self.num\_rounds + 1): # Iterate through all rounds in the tournament
 round\_win\_counts = self.simulate\_round\_n(round\_num, num\_simulations)
 self.print\_round\_results(round\_num, round\_win\_counts, num\_simulations)

 @staticmethod
 def error\_function(k, average\_kenpom\_difference):
 error = 0
 for matchup, historical\_probability in HISTORICAL\_SEED\_WIN\_RATES.items():
 difference = average\_kenpom\_difference[matchup]
 probability = 1 / (1 + exp(-k \* difference))
 error += (probability - historical\_probability) \*\* 2
 return error

 @staticmethod
 def average\_kenpom\_difference(max\_seed=16, kenpom\_range=(0, 40)):
 min\_kenpom, max\_kenpom = kenpom\_range
 kenpom\_increment = (max\_kenpom - min\_kenpom) / max\_seed
 average\_difference = {}

 for higher\_seed in range(1, max\_seed + 1):
 for lower\_seed in range(higher\_seed + 1, max\_seed + 1):
 higher\_seed\_kenpom = max\_kenpom - (higher\_seed - 1) \* kenpom\_increment
 lower\_seed\_kenpom = max\_kenpom - (lower\_seed - 1) \* kenpom\_increment
 average\_difference[(higher\_seed, lower\_seed)] = higher\_seed\_kenpom - lower\_seed\_kenpom

 return average\_difference

 @staticmethod
 def find\_best\_k():
 average\_difference = Tournament.average\_kenpom\_difference()
 result = minimize\_scalar(Tournament.error\_function, args=(average\_difference,))
 return result.x
if \_\_name\_\_ == "\_\_main\_\_":
 parser = argparse.ArgumentParser(
 description="NCAA Tournament Simulation"
 )
 parser.add\_argument(
 '-f', '--file',
 default='2023ncaab.csv',
 help="Path to the data file (default: '2023ncaab.csv')"
 )
 parser.add\_argument(
 '-n', '--num-simulations',
 type=int,
 default=100000,
 help="Number of simulations to run (default: 100000)"
 )
 args = parser.parse\_args()
 teams = Team.extract\_teams(args.file)
 tournament = Tournament(teams)
 tournament.run\_all\_rounds(args.num\_simulations)
mport argparse임포트 CSV임포트 수학import os컬렉션에서 카운터 임포트입력에서 임포트 목록데이터 클래스 임포트numpy를 np로 임포트numpy.ma에서 exp 임포트scipy.optimize에서 가져 오기 최소화\_스칼라from scipy.special import expitPARENT\_DIR = os.path.dirname(os.path.realpath(\_\_file\_\_))historical\_seed\_win\_rates = { (1, 16): 1.000, (2, 15): 0.917, (3, 14): 0.833, (4, 13): 0.750, (5, 12): 0.667, (6, 11): 0.583, (7, 10): 0.583, (8, 9): 0.500,}@dataclasses.dataclass팀 클래스: team\_name: str 팀\_씨앗: int ken\_pom\_score: float @classmethod def extract\_teams(cls, file\_path: str): with open(os.path.join(PARENT\_DIR, file\_path), "r", newline="") as csvfile: return [cls( team\_name=row["team"], team\_seed=int(row["seed"]), ken\_pom\_score=float(row["score"]) ) for row in (csv.DictReader(csvfile))]토너먼트 클래스: def \_\_init\_\_(self, team\_metrics: List[Team]): self.team\_metrics: List[Team] = team\_metrics self.team\_names = [team\_metrics의 팀에 대한 팀.팀\_이름] self.teams = {i: 팀에 대한 팀.팀\_이름, 팀에 대한 열거(팀\_메트릭)} self.k = self.find\_best\_k() self.adj\_matrix = self.calculate\_adj\_matrix() num\_teams = len(self.teams) self.num\_rounds = int(math.log2(num\_teams)) self.round\_winners = {0: list(self.teams.keys())} self.round\_win\_counts = {} # 대진표 구성 num\_matches = num\_teams // 2 self.bracket = {} for round\_num in range(1, self.num\_rounds + 1): self.bracket[round\_num] = list(range(num\_matches)) num\_matches //= 2 # 결과 인쇄에 사용 self.round\_names = { 1: "64의 라운드", 2: "32강" 3: "스위트 식스틴", 4: "엘리트 8강", 5: "파이널 4", 6: "챔피언십", } self.round\_spacing = { 1: 2, 2: 4, 3: 8, 4: 16, 5: 32, 6: 64, } def get\_opponent(self, round\_num, team\_index): if round\_num == 0: opponent\_index = team\_index + 1 if team\_index % 2 == 0 else team\_index - 1 else: 이전\_라운드\_승자 = self.round\_승자[round\_num - 1] # print(f"이전 라운드 우승자: {previous\_round\_winners}") # 디버깅 인쇄 팀\_포지션 = 이전\_라운드\_승자.index(팀\_인덱스) 상대팀\_인덱스 = 이전\_라운드\_승자[팀\_포지션 + 1 if 팀\_포지션 % 2 == 0 else 팀\_포지션 - 1] # print(f"팀 인덱스: {team\_index}, 상대 인덱스: {opponent\_index}") # 디버깅 인쇄 반환 상대\_인덱스 def calculate\_adj\_matrix(self): num\_teams = len(self.team\_metrics) adj\_matrix = np.zeros((num\_teams, num\_teams)) for i, team\_i in enumerate(self.team\_metrics): for j, team\_j in enumerate(self.team\_metrics): if i != j: p\_win = self.calculate\_win\_probability(team\_i, team\_j) adj\_matrix[i, j] = p\_win adj\_matrix[j, i] = 1 - p\_win 반환 adj\_matrix def calculate\_win\_probability(self, team\_i: 팀, team\_j: 팀): seed\_diff = team\_j.team\_seed - team\_i.team\_seed ken\_pom\_diff = team\_i.ken\_pom\_score - team\_j.ken\_pom\_score return expit(self.k \* (ken\_pom\_diff + seed\_diff)) def play\_single\_round(self, remaining\_teams): winners = [] FOR I IN RANGE(0, LEN(REMAINING\_TEAMS), 2): team\_i = remaining\_teams[i] team\_j = 남은\_팀[i + 1] p\_win\_i = self.adj\_matrix[team\_i, team\_j] win\_i = np.random.rand() < p\_win\_i winning\_team\_index = team\_i if win\_i else team\_j winners.append(winning\_team\_index) 우승자 반환 def get\_remaining\_teams(self, round\_num): if round\_num == 0: return list(range(len(self.teams))) remaining\_teams = [] for i, team in enumerate(self.teams): if self.round\_winners[round\_num - 1].count(team) > 0: remaining\_teams.append(i) 반환 남은\_팀 def simulate\_round\_n(self, round\_num, num\_simulations): round\_win\_counts = Counter() for \_ in range(num\_simulations): 남은\_팀 = self.get\_remaining\_팀(round\_num) winners = self.play\_single\_round(remaining\_teams) 승자의 경우 라운드\_승리\_counts[self.teams[winner]] += 1 # 라운드\_승자 및 라운드\_승수\_count 업데이트 sorted\_teams = sorted(round\_win\_counts.items(), key=lambda x: x[1], reverse=True) self.round\_winners[round\_num] = [list(self.teams.keys())[list(self.teams.values()).index(team)] for team, \_ in sorted\_teams] self.round\_win\_counts[round\_num] = round\_win\_counts 반환 라운드\_승수\_counts def print\_round\_results(self, round\_num, round\_win\_counts, num\_simulations): print(f"{self.round\_names[round\_num]} results:") for i, (team\_name, count) in enumerate(round\_win\_counts.most\_common()): team\_index = list(self.teams.values()).index(team\_name) 팀 = self.teams[team\_index] 상대\_인덱스 = self.get\_opponent(라운드\_넘버, 팀\_인덱스) 상대 = self.teams[상대\_index] win\_percentage = (카운트/넘수\_simulations) \* 100 spaces = self.round\_spacing[round\_num] - len(str(i + 1)) print(f" {i + 1}.{' ' \* spaces}{team} over {opponent} with {win\_percentage:.2f}%") print() def run\_all\_rounds(self, nu