in general anytime a kenpom score is lower and a team wins, it is an upset. Lets print aggregate upset stats in our code at each round

import argparse
import csv
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
import math

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, teams: List[Team], num\_simulations: int):
 self.teams: List[Team] = teams
 self.num\_simulations = num\_simulations
 self.k = self.find\_best\_k()
 self.adj\_matrix = self.calculate\_adj\_matrix()

 @staticmethod
 def get\_opponent\_index(team\_index):
 return team\_index + 1 if team\_index % 2 == 0 else team\_index - 1

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

 for i, team\_i in enumerate(self.teams):
 for j, team\_j in enumerate(self.teams):
 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 run(self):
 print(f"\nRound of {len(self.teams)}")
 if len(self.teams) == 0:
 print("No teams in the tournament. Exiting.")
 return

 if len(self.teams) == 1:
 print(f"Winner: {self.teams[0].team\_name}")
 return

 winners = self.play\_round()
 updated\_tournament = Tournament(winners, self.num\_simulations)
 updated\_tournament.run()

 def play\_round(self):
 round\_win\_counts = Counter()
 for \_ in range(int(self.num\_simulations \* 64 / len(self.teams))):
 for i in range(0, len(self.teams), 2):
 p\_win\_i = self.adj\_matrix[i, i + 1]
 win\_i = np.random.rand() < p\_win\_i
 winning\_team\_index = i if win\_i else i + 1
 round\_win\_counts[winning\_team\_index] += 1
 winning\_team\_indices = []

 for i in range(0, len(self.teams), 2):
 index = i if round\_win\_counts[i] > round\_win\_counts[i + 1] else i + 1
 print(f"({self.teams[i].team\_name}, {self.teams[i + 1].team\_name})->{self.teams[index].team\_name}")
 winning\_team\_indices.append(index)

 winning\_teams: list[Team] = [self.teams[winner\_index] for winner\_index in winning\_team\_indices]

 return winning\_teams

 def get\_team\_by\_name(self, team\_name: str):
 for team in self.teams:
 if team.team\_name == team\_name:
 return team
 return None

 def calculate\_win\_probability(self, team\_i: Team, team\_j: Team):
 ken\_pom\_diff = team\_i.ken\_pom\_score - team\_j.ken\_pom\_score
 probability = 1 / (1 + math.exp(-self.k \* ken\_pom\_diff))
 return probability

 @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=1000,
 help="Number of simulations to run (default: 100000)"
 )
 args = parser.parse\_args()
 teams\_ = Team.extract\_teams(args.file)
 tournament = Tournament(teams\_, args.num\_simulations)
 tournament.run()
일반적으로 켄펌 점수가 낮고 한 팀이 승리하면 업셋입니다. 각 라운드마다 코드에서 집계된 업셋 통계를 출력해 보겠습니다.import argparseimport csvimport os컬렉션에서 가져오기 카운터에서 임포트 목록데이터 클래스 임포트numpy를 np로 임포트numpy.ma에서 exp 임포트scipy.optimize에서 가져 오기 최소화\_스칼라import mathPARENT\_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, teams: List[Team], num\_simulations: int): self.teams: List[Team] = teams self.num\_simulations = num\_simulations self.k = self.find\_best\_k() self.adj\_matrix = self.calculate\_adj\_matrix() @staticmethod def get\_opponent\_index(team\_index): return team\_index + 1 if team\_index % 2 == 0 else team\_index - 1 def calculate\_adj\_matrix(self): num\_teams = len(self.teams) adj\_matrix = np.zeros((num\_teams, num\_teams)) for i, team\_i in enumerate(self.teams): for j, team\_j in enumerate(self.teams): 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 run(self): print(f"\n{len(self.teams)}의 라운드") len(self.teams) == 0: print("토너먼트에 참가팀이 없습니다. 종료합니다.") 반환 len(self.teams) == 1: print(f"우승자: {self.teams[0].team\_name}") 반환 winners = self.play\_round() updated\_tournament = 토너먼트(우승자, self.num\_simulations) updated\_tournament.run() def play\_round(self): round\_win\_counts = Counter() for \_ in range(int(self.num\_simulations \* 64 / len(self.teams))): for i in range(0, len(self.teams), 2): p\_win\_i = self.adj\_matrix[i, i + 1] win\_i = np.random.rand() < p\_win\_i winning\_team\_index = i if win\_i else i + 1 round\_win\_counts[winning\_team\_index] += 1 winning\_team\_indices = [] for i in range(0, len(self.teams), 2): index = i if round\_win\_counts[i] > round\_win\_counts[i + 1] else i + 1 print(f"({self.teams[i].team\_name}, {self.teams[i + 1].team\_name})->{self.teams[index].team\_name}") winning\_team\_indices.append(index) winning\_teams: list[Team] = [self.teams[winner\_index] for winner\_index in winning\_team\_indices] return winning\_teams def get\_team\_by\_name(self, team\_name: str): for team in self.teams: if team.team\_name == team\_name: 반환 팀 반환 없음 def calculate\_win\_probability(self, team\_i: 팀, team\_j: 팀): ken\_pom\_diff = team\_i.ken\_pom\_score - team\_j.ken\_pom\_score probability = 1 / (1 + math.exp(-self.k \* ken\_pom\_diff)) 확률 반환 정적 메서드 def error\_function(k, average\_kenpom\_difference): error = 0 매치업의 경우, HISTORICAL\_SEED\_WIN\_RATES.items()의 historical\_probability: 차이 = 평균\_켄폼\_차이[매치업] 확률 = 1 / (1 + exp(-k \* 차이)) error += (확률 - 과거\_확률) \*\* 2 반환 오류 정적 메서드 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 평균\_차이 = {} 범위(1, 최대\_씨드 + 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 반환 평균\_difference @staticmethod def find\_best\_k(): average\_difference = Tournament.average\_kenpom\_difference() result = minimize\_scalar(Tournament.error\_function, args=(average\_difference,)) 반환 결과.xif \_\_name\_\_ == "\_\_main\_\_": parser = argparse.ArgumentParser( description="NCAA 토너먼트 시뮬레이션" ) parser.add\_argument( '-f', '--file', default='2023ncaab.csv', help="데이터 파일 경로(기본값: '2023ncaab.csv')" ) parser.add\_argument( '-n', '--num-simulations', type=int, default=1000, help="실행할 시뮬레이션 수(기본값: 100000)" ) args = 파서.파싱\_args() teams\_ = Team.extract\_teams(args.file) 토너먼트 = 토너먼트(teams\_, args.num\_simulations) tournament.run()