<jupyter_start><jupyter_text>Undirected networks- Pace- Rebounds<jupyter_code># 1. Load Data

# get opponent team id
def get_opponent_team_id(data):
    
    opps = data.copy()[['game_id','team_id']].drop_duplicates().reset_index(drop=True)
    opps['team_AorB'] = opps.groupby(['game_id'])['team_id'].rank('dense').astype(int).map({
        1:'A',
        2:'B'
    })
    opps = opps.pivot(index='game_id', columns=['team_AorB'], values='team_id').reset_index()
    opps.columns=['game_id','team_id','opp_id']
    opps2 = opps.copy()
    opps2.columns=['game_id','opp_id','team_id']
    opps = pd.concat([opps, opps2], axis=0).dropna().reset_index(drop=True)
    opps['team_id'] = opps['team_id'].astype(int)
    opps['opp_id'] = opps['opp_id'].astype(int)
    
    return opps




def get_possessions(pbox_data):
    
    opponent_ids = get_opponent_team_id(pbox_data.copy())
    ## estimate number of possessions from box score 
    poss = pbox_data.groupby(['game_id','team_id'])[['fga','to','fta','oreb']].sum().reset_index()
    ## commonly used possession estimate formula
    ## (FGA – OR) + TO + (0.44 * FTA)
    poss['tm_poss'] = (poss['fga'].copy()-poss['oreb'].copy())+poss['to'].copy()+(0.44*poss['fta'].copy())
    poss = poss.drop(columns=['fga','to','fta','oreb'])

    possession_key = opponent_ids.copy().merge(poss, how='left', on=['game_id','team_id'])
    poss = poss.rename(columns={'team_id':'opp_id','tm_poss':'opp_poss'})
    possession_key = possession_key.copy().merge(poss, how='left', on=['game_id','opp_id'])
    possession_key['game_possessions'] = possession_key[['tm_poss','opp_poss']].copy().mean(axis=1)
    possession_key = possession_key.drop(columns=['tm_poss','opp_poss','opp_id'])

    return possession_key.sort_values(by='game_id').reset_index(drop=True)

def add_player_boxscore_features(data):
    
    #pbox
    data['fgm'] = data['fg'].apply(lambda x: x.split('-')[0])
    data['fga'] = data['fg'].apply(lambda x: x.split('-')[-1])
    data['fg3m'] = data['fg3'].apply(lambda x: x.split('-')[0])
    data['fg3a'] = data['fg3'].apply(lambda x: x.split('-')[-1])
    data['ftm'] = data['ft'].apply(lambda x: x.split('-')[0])
    data['fta'] = data['ft'].apply(lambda x: x.split('-')[-1])

    data['fgm']= data['fgm'].replace('',0)
    data['fgm'] = data['fgm'].astype(int)
    data['fga']= data['fga'].replace('',0)
    data['fga'] = data['fga'].astype(int)
    data['ftm']= data['ftm'].replace('',0)
    data['ftm'] = data['ftm'].astype(int)
    data['fta']= data['fta'].replace('',0)
    data['fta'] = data['fta'].astype(int)
    
    data['oreb']= data['oreb'].replace('',0)
    data['oreb'] = data['oreb'].astype(int)
    data['dreb']= data['dreb'].replace('',0)
    data['dreb'] = data['dreb'].astype(int)
    data['reb']= data['reb'].replace('',0)
    data['reb'] = data['reb'].astype(int)

    data['fg3m']= data['fg3m'].replace('',0)
    data['fg3m'] = data['fg3m'].astype(int)
    data['fg3a']= data['fg3a'].replace('',0)
    data['fg3a'] = data['fg3a'].astype(int)

    data['fg2m'] = data['fgm'].copy()-data['fg3m'].copy()
    data['fg2a'] = data['fga'].copy()-data['fg3a'].copy()
    
    possess = get_possessions(data.copy())
    data = data.merge(possess, how='left', on=['game_id','team_id'])

    data['fg%'] = (data['fgm'].copy()/data['fga'].copy()).fillna(0)
    data['fg2%'] = (data['fg2m'].copy()/data['fg2a'].copy()).fillna(0)
    data['fg3%'] = (data['fg3m'].copy()/data['fg3a'].copy()).fillna(0)

    data['eFG%'] = ((data['fgm'].copy()+(data['fg3m'].copy()*0.5))/data['fga'].copy()).fillna(0)
    data['TS%'] = ((data['pts'].copy())/(2*(data['fga'].copy()+(0.44*data['fta'].copy())))).fillna(0)
    # pbox[['fg','fg3m','fga']].dtypes
    data['pts_pm'] = data['pts'].copy()/data['min'].copy()
    data['reb_pm'] = data['reb'].copy()/data['min'].copy()
    data['ast_pm'] = data['ast'].copy()/data['min'].copy()
    data['stl_pm'] = data['stl'].copy()/data['min'].copy()
    data['blk_pm'] = data['blk'].copy()/data['min'].copy()
    data['to_pm'] = data['to'].copy()/data['min'].copy()
    data['pf_pm'] = data['pf'].copy()/data['min'].copy()
    
    ## could be improved with OT markers
    ## percentage of estimated possessions player took part of
    data['player_possessions'] = data['game_possessions'].copy()*(data['min'].copy()/(40*2)) # times 2 because game possessions = 
    
    data['pts_pp'] = data['pts'].copy()/data['player_possessions'].copy()
    data['reb_pp'] = data['reb'].copy()/data['player_possessions'].copy()
    data['ast_pp'] = data['ast'].copy()/data['player_possessions'].copy()
    data['stl_pp'] = data['stl'].copy()/data['player_possessions'].copy()
    data['blk_pp'] = data['blk'].copy()/data['player_possessions'].copy()
    data['to_pp'] = data['to'].copy()/data['player_possessions'].copy()
    data['pf_pp'] = data['pf'].copy()/data['player_possessions'].copy()
    
    return data


def clean_player_boxscores(data):
    for stat_col in ['min','pts','oreb','dreb','reb','ast','stl','blk','to','pf']:
        data[stat_col] = data[stat_col].replace('--',0)
        data[stat_col] = data[stat_col].astype(int)

    return data


def load_player_boxscore_season(year):
    return add_player_boxscore_features(\
            clean_player_boxscores(\
            pd.read_csv(os.path.join(DATA_PATH, f'ESPN/player_boxscores/{year}.csv'))))


## used for visualizations
def rgb_to_hsl(r, g, b):
    r = float(r)
    g = float(g)
    b = float(b)
    high = max(r, g, b)
    low = min(r, g, b)
    h, s, v = ((high + low) / 2,)*3

    if high == low:
        h = 0.0
        s = 0.0
    else:
        d = high - low
        s = d / (2 - high - low) if l > 0.5 else d / (high + low)
        h = {
            r: (g - b) / d + (6 if g < b else 0),
            g: (b - r) / d + 2,
            b: (r - g) / d + 4,
        }[high]
        h /= 6

    return h, s, v

def complementaryColor(my_hex):
    if my_hex[0] == '#':
        my_hex = my_hex[1:]
    rgb = (my_hex[0:2], my_hex[2:4], my_hex[4:6])
    comp = ['%02X' % (255 - int(a, 16)) for a in rgb]
    return ''.join(comp)

def determine_darker_color(c1, c2):
    r, g, b = c1
    r2, g2, b2 = c2
    hsp1 = 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)
    hsp2 = 0.299 * (r2 * r2) + 0.587 * (g2 * g2) + 0.114 * (b2 * b2)
    
    if hsp1 > hsp2:
        # darker is hsp2
        return 0
    elif hsp1 < hsp2:
        return 1
    else:
        print(hsp1, hsp2)
        raise ValueError() # same color


def load_colors():
    
    team_meta = pd.read_csv('team_meta.csv')
    teams_id2conf = team_meta.copy().drop_duplicates(subset=['ESPN_team_id'])[['ESPN_team_id','conference_name']].set_index('ESPN_team_id').to_dict()['conference_name']

    # fill nas with complementary
    # don't want these being the same
    team_meta['secondary_color'] = np.where(team_meta['primary_color']==team_meta['secondary_color'], np.nan, team_meta['secondary_color'].copy())
    # one special case
    team_meta.loc[team_meta['ESPN_team_id']==57, 'secondary_color'] = 'FA4616'
    team_meta['rgb_primary'] = team_meta['primary_color'].apply(lambda x: to_rgb('#'+x))
    team_meta['secondary_color'] = team_meta['secondary_color'].fillna(team_meta['primary_color'].apply(lambda x: complementaryColor(x)))
    team_meta['rgb_secondary'] = team_meta['secondary_color'].apply(lambda x: to_rgb('#'+str(x)))
    team_meta['primary_darker'] = team_meta.apply(lambda x: determine_darker_color(x.rgb_primary, x.rgb_secondary), axis=1)

    team_meta['darker_color'] = np.where(team_meta['primary_darker']==1, team_meta['primary_color'].copy(), team_meta['secondary_color'].copy())
    team_meta['lighter_color'] = np.where(team_meta['primary_darker']==0, team_meta['primary_color'].copy(), team_meta['secondary_color'].copy())
    team_meta['darker_color'] = '#' + team_meta['darker_color'].copy()
    team_meta['lighter_color'] = '#' + team_meta['lighter_color'].copy()

    team_dark = team_meta.copy()[['ESPN_team_id','darker_color']].set_index('ESPN_team_id').to_dict()['darker_color']
    team_light = team_meta.copy()[['ESPN_team_id','lighter_color']].set_index('ESPN_team_id').to_dict()['lighter_color']

    return team_dark, team_light


pbox = load_player_boxscore_season(2022)
opponent_ids = get_opponent_team_id(pbox.copy())
## create name mapping
players_id2name = pbox.copy().drop_duplicates(subset=['athlete_id'])[['athlete_id','athlete_display_name']].set_index('athlete_id').to_dict()['athlete_display_name']
players_name2id = {v:k for k,v in players_id2name.items()}

players_id2team = pbox.copy().drop_duplicates(subset=['athlete_id'],keep='last')[['athlete_id','team_short_display_name']].set_index('athlete_id').to_dict()['team_short_display_name']

teams_id2name = pbox.copy().drop_duplicates(subset=['team_id'])[['team_id','team_short_display_name']].set_index('team_id').to_dict()['team_short_display_name']
teams_name2id = {v:k for k,v in teams_id2name.items()}

# save_dict(existing_dict, os.path.join(DATA_PATH, 'IDs/kenpom2tname'))
# save_dict({v:k for k,v in existing_dict.items()}, os.path.join(DATA_PATH, 'IDs/tname2kenpom'))

team_dark, team_light = load_colors()
conferences = pd.read_csv('team_meta.csv')



# save_dict(teams_id2name, os.path.join(DATA_PATH, 'IDs/teams_id2name'))
# save_dict(teams_name2id, os.path.join(DATA_PATH, 'IDs/teams_name2id'))
# save_dict(teams_id2name, '../../src/network_viz_data/teams_id2name')
# save_dict(teams_name2id, '../../src/network_viz_data/teams_name2id')<jupyter_output><empty_output><jupyter_text>Pace Network<jupyter_code># need to create possession estimates
def create_pace_net(data):
    
    opp_data = get_opponent_team_id(data.copy())
    game_possessions = get_possessions(data.copy())
    net_data = pd.merge(opp_data.copy(), game_possessions.copy(), how='left', on=['game_id','team_id'])
    net_data = net_data.drop_duplicates(subset=['game_id']).reset_index(drop=True)
    net_data['team_id'] = net_data['team_id'].astype(int)
    net_data['opp_id'] = net_data['opp_id'].astype(int)
    
    ## scale weight of edges
    mms = MinMaxScaler()
    net_data['game_possessions'] = mms.fit_transform(net_data['game_possessions'].values.reshape(-1,1))
    
    nodes = list(set(net_data['team_id'].unique()).union(set(net_data.opp_id.unique())))
    edges = [tuple([int(e[0]),int(e[1]),e[2]]) for e in net_data[['team_id','opp_id','game_possessions']].values.copy()]

    pace_net = nx.MultiGraph()
    pace_net.add_nodes_from(nodes)
    pace_net.add_weighted_edges_from(edges)
    pace_ranks = pd.Series(nx.pagerank(pace_net,alpha=1)).reset_index()
    pace_ranks.columns=['team_id','prank']
    pace_ranks['team_name'] = pace_ranks['team_id'].map(teams_id2name)
    
    degrees = pd.DataFrame.from_dict(pace_net.degree())
    degrees.columns=['team_id','degree']
    pace_ranks = pace_ranks.copy().merge(degrees, how='left', on='team_id')
    pace_ranks['pace_rating'] = 100000* (pace_ranks['prank'].copy()/pace_ranks['degree'].copy())
    pace_ranks = pace_ranks.sort_values(by='pace_rating', ascending=False)
    
    pace_ranks = pace_ranks.loc[pace_ranks['degree']>22].drop(columns=['prank','degree'])  # minimum 25 games played (some DII teams show up otherwise)
    
    pace_ranks['pace_rank'] =pace_ranks['pace_rating'].rank(method='dense', ascending=False)
    pace_ranks['pace_rank']=pace_ranks['pace_rank'].astype(int)
    return pace_ranks.reset_index(drop=True), pace_net

pace_series, pace_network = create_pace_net(pbox.copy())
pace_series = pace_series.merge(conferences.rename(columns={'ESPN_team_id':'team_id'})[['team_id','ESPN_conference_id','conference_name']])



def load_kenpom(season):
    kp = pd.read_csv(os.path.join(DATA_PATH, f'kenpom/{season}.csv'))
    return kp.rename(columns={'TeamName':'kenpom_name'})
k22 = load_kenpom(2022)<jupyter_output><empty_output><jupyter_text>Need to name match with KenPom to show rankings from there<jupyter_code># existing_map = {} # starts over
ambiguous = []


def name_matching(names, choices, existing):
    
    for i, name in enumerate(names):
        if i % 25 == 0:
            print(f"We are through {i} teams")
        if name in existing:
            continue
        top_5 = process.extract(name, [en for en in choices if en not in list(existing.values())], limit=8)
        if top_5[0][1] >= 98:
            existing[name] = top_5[0][0]
            print(f"{name} == {top_5[0][0]}")
        else: 
            ## ask
            print("See any matches? Use 0 or blank to continue.")
            print(name, "         ", top_5)
            print("Otherwise use numbers 1-8")
            resp = input()
            if not resp.isdigit():
                print("invalid response, skipping")
                ambiguous.append(name) 
            elif int(resp)>8:
                print("invalid response, skipping")
                ambiguous.append(name) 
            elif int(resp)==0:
                print("skipping")
                ambiguous.append(name) 
            elif int(resp)=='':
                print("skipping")
                ambiguous.append(name) 
            else:
                resp = int(resp)
                existing[name] = top_5[resp-1][0]
                print(f"{name} == {top_5[resp-1][0]}")
    
    return existing

# knames = list(k22['kenpom_name'].unique())
# espn_names = list(pace_series['team_name'].unique())

# existing_dict = load_dict(os.path.join(DATA_PATH, 'IDs/kenpom2tname'))
# existing_dict = name_matching(knames, espn_names, existing_dict) # if new names are needed


# save_dict(existing_dict, os.path.join(DATA_PATH, 'IDs/kenpom2tname'))
# save_dict({v:k for k,v in existing_dict.items()}, os.path.join(DATA_PATH, 'IDs/tname2kenpom'))


# for streamlit
# sorted(pace_series.conference_name.unique())<jupyter_output><empty_output><jupyter_text>Code for graphing conference undirected pace graphs<jupyter_code>tname2kenpom = load_dict(os.path.join(DATA_PATH, 'IDs/tname2kenpom'))
pace_series['kenpom_name'] = pace_series['team_name'].map(tname2kenpom)
## merge kenpom
pace_series = pace_series.merge(k22[['kenpom_name','Tempo','AdjTempo','RankTempo','RankAdjTempo']].copy(), how='left', on='kenpom_name')
pace_series[['pace_rank','RankTempo','RankAdjTempo']].corr('spearman')


nx.write_gml(pace_network, "../../src/network_viz_data/pace_graph.gml", stringizer=str)
pace_network = nx.read_gml('../../src/network_viz_data/pace_graph.gml')
# Read graph
# pace_series.to_csv('../../src/network_viz_data/pace_df.csv',index=False)
subteams = pace_series.copy().loc[pace_series['conference_name']=='ACC'].reset_index(drop=True)
subnodes = list(subteams['team_id'].unique())
sub_g = pace_network.subgraph([int(sn) for sn in subnodes])
sub_g.nodes()
# nx.draw(sub_g)

plt.style.use('ggplot')
def draw_pace_subgraph(pace_df, conf):
    
    # pace_series
    subteams = pace_df.copy().loc[pace_df['conference_name']==conf].reset_index(drop=True)
    prnk = pace_df.copy().set_index('team_id').to_dict()['pace_rating']
    subnodes = list(subteams['team_id'].unique())

    sub_g = pace_network.subgraph([str(sn) for sn in subnodes])

    fig, axes= plt.subplots(2,1, figsize=(14, 18))
    ax1 = axes[0]
    sub_wedges = sub_g.edges(data="weight")
    sub_edges = sub_g.edges()
    sub_nodes = sub_g.nodes()

    pos = nx.circular_layout(sub_g)
    weights = [e * 5 for u,v,e in sub_wedges]
    colors = [team_dark[int(n)] for n in sub_nodes]
    # fig, ax = plt.subplots(figsize=(22,16))
    nx.draw(sub_g, edge_color=weights, pos=pos, labels={n:teams_id2name[int(n)] for n in sub_nodes}, 
            node_color=[prnk[int(n)] for n in sub_nodes], node_size=[7e2*prnk[int(n)] for n in sub_nodes], font_size=10, font_color="black",
           cmap=plt.cm.bwr, edge_cmap=plt.cm.bwr, ax=ax1)

    ax2=axes[1]
    ax2.set_title("Pace Network vs. KenPom \nPace Ratings",fontsize=24)
    ax2.set_xlabel("Pace Network\nPageRank", fontsize=16)
    ax2.set_ylabel("KenPom Adjusted\n Tempo", fontsize=16)
    ax2.scatter(subteams.pace_rating, subteams.Tempo, c=[prnk[int(n)] for n in subteams.team_id.values],
               cmap=plt.cm.bwr, s=350)
    n = subteams.team_name
    for i, txt in enumerate(n):
        ax2.annotate(txt, (subteams.pace_rating[i]+0.03, subteams.Tempo[i]))

    plt.show()
    
    return

draw_pace_subgraph(pace_series, 'SEC')




pace_series.loc[pace_series['conference_name']=='ACC']


pbox


prnk[153]



# nx.draw(pace_network)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.19570115082506243, 'fraction_numerical': 0.01698836996894599, 'mean_word_length': 4.342875731945348, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 22, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '666743', 'license': 'MIT', 'n_tokens_mistral': 6372, 'n_tokens_neox': 6172, 'n_words': 876, 'path': 'notebooks/visualizations/streamlit_network_page.ipynb', 'repo_name': 'btatkinson/NCAA-basketball-Capstone', 'size': '16423'}