diff --git a/database/L1A/L1A.sqlite b/database/L1A/L1A.sqlite index d6b6f98..09d14c9 100644 Binary files a/database/L1A/L1A.sqlite and b/database/L1A/L1A.sqlite differ diff --git a/database/L2/L2.db b/database/L2/L2.db deleted file mode 100644 index e69de29..0000000 diff --git a/database/L2/L2_Main.sqlite b/database/L2/L2_Main.sqlite index dc0426d..81f8ab7 100644 Binary files a/database/L2/L2_Main.sqlite and b/database/L2/L2_Main.sqlite differ diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index cdfecfa..0000000 --- a/scripts/README.md +++ /dev/null @@ -1 +0,0 @@ -用于测试脚本目录。 \ No newline at end of file diff --git a/scripts/analyze_features.py b/scripts/analyze_features.py deleted file mode 100644 index ab77cd3..0000000 --- a/scripts/analyze_features.py +++ /dev/null @@ -1,214 +0,0 @@ -import sqlite3 -import pandas as pd -import numpy as np -import os - -DB_L2_PATH = r'd:\Documents\trae_projects\yrtv\database\L2\L2_Main.sqlite' - -def get_db_connection(): - conn = sqlite3.connect(DB_L2_PATH) - conn.row_factory = sqlite3.Row - return conn - -def load_data_and_calculate(conn, min_matches=5): - print("Loading Basic Stats...") - - # 1. Basic Stats - query_basic = """ - SELECT - steam_id_64, - COUNT(*) as matches_played, - AVG(rating) as avg_rating, - AVG(kd_ratio) as avg_kd, - AVG(adr) as avg_adr, - AVG(kast) as avg_kast, - SUM(first_kill) as total_fk, - SUM(first_death) as total_fd, - SUM(clutch_1v1) + SUM(clutch_1v2) + SUM(clutch_1v3) + SUM(clutch_1v4) + SUM(clutch_1v5) as total_clutches, - SUM(throw_harm) as total_util_dmg, - SUM(flash_time) as total_flash_time, - SUM(flash_enemy) as total_flash_enemy - FROM fact_match_players - GROUP BY steam_id_64 - HAVING COUNT(*) >= ? - """ - df_basic = pd.read_sql_query(query_basic, conn, params=(min_matches,)) - - valid_ids = tuple(df_basic['steam_id_64'].tolist()) - if not valid_ids: - print("No players found.") - return None - placeholders = ','.join(['?'] * len(valid_ids)) - - # 2. Side Stats (T/CT) via Economy Table (which has side info) - print("Loading Side Stats via Round Map...") - # Map each round+player to a side - query_side_map = f""" - SELECT match_id, round_num, steam_id_64, side - FROM fact_round_player_economy - WHERE steam_id_64 IN ({placeholders}) - """ - try: - df_sides = pd.read_sql_query(query_side_map, conn, params=valid_ids) - - # Get all Kills - query_kills = f""" - SELECT match_id, round_num, attacker_steam_id as steam_id_64, COUNT(*) as kills - FROM fact_round_events - WHERE event_type = 'kill' - AND attacker_steam_id IN ({placeholders}) - GROUP BY match_id, round_num, attacker_steam_id - """ - df_kills = pd.read_sql_query(query_kills, conn, params=valid_ids) - - # Merge to get Kills per Side - df_merged = df_kills.merge(df_sides, on=['match_id', 'round_num', 'steam_id_64'], how='inner') - - # Aggregate - side_stats = df_merged.groupby(['steam_id_64', 'side'])['kills'].sum().unstack(fill_value=0) - side_stats.columns = [f'kills_{c.lower()}' for c in side_stats.columns] - - # Also need deaths to calc KD (approx) - # Assuming deaths are in events as victim - query_deaths = f""" - SELECT match_id, round_num, victim_steam_id as steam_id_64, COUNT(*) as deaths - FROM fact_round_events - WHERE event_type = 'kill' - AND victim_steam_id IN ({placeholders}) - GROUP BY match_id, round_num, victim_steam_id - """ - df_deaths = pd.read_sql_query(query_deaths, conn, params=valid_ids) - df_merged_d = df_deaths.merge(df_sides, on=['match_id', 'round_num', 'steam_id_64'], how='inner') - side_stats_d = df_merged_d.groupby(['steam_id_64', 'side'])['deaths'].sum().unstack(fill_value=0) - side_stats_d.columns = [f'deaths_{c.lower()}' for c in side_stats_d.columns] - - # Combine - df_side_final = side_stats.join(side_stats_d).fillna(0) - df_side_final['ct_kd'] = df_side_final.get('kills_ct', 0) / df_side_final.get('deaths_ct', 1).replace(0, 1) - df_side_final['t_kd'] = df_side_final.get('kills_t', 0) / df_side_final.get('deaths_t', 1).replace(0, 1) - - except Exception as e: - print(f"Side stats failed: {e}") - df_side_final = pd.DataFrame({'steam_id_64': list(valid_ids)}) - - # 3. PTL (Pistol) via Rounds 1 and 13 - print("Loading Pistol Stats via Rounds...") - query_pistol_kills = f""" - SELECT - ev.attacker_steam_id as steam_id_64, - COUNT(*) as pistol_kills - FROM fact_round_events ev - WHERE ev.attacker_steam_id IN ({placeholders}) - AND ev.event_type = 'kill' - AND ev.round_num IN (1, 13) - GROUP BY ev.attacker_steam_id - """ - df_ptl = pd.read_sql_query(query_pistol_kills, conn, params=valid_ids) - - # 4. HPS - print("Loading HPS Stats...") - query_close = f""" - SELECT mp.steam_id_64, AVG(mp.rating) as close_match_rating - FROM fact_match_players mp - JOIN fact_matches m ON mp.match_id = m.match_id - WHERE mp.steam_id_64 IN ({placeholders}) - AND ABS(m.score_team1 - m.score_team2) <= 3 - GROUP BY mp.steam_id_64 - """ - df_hps = pd.read_sql_query(query_close, conn, params=valid_ids) - - # 5. STA - query_sta = f""" - SELECT mp.steam_id_64, mp.rating, mp.is_win - FROM fact_match_players mp - WHERE mp.steam_id_64 IN ({placeholders}) - """ - df_matches = pd.read_sql_query(query_sta, conn, params=valid_ids) - sta_data = [] - for pid, group in df_matches.groupby('steam_id_64'): - rating_std = group['rating'].std() - win_rating = group[group['is_win']==1]['rating'].mean() - loss_rating = group[group['is_win']==0]['rating'].mean() - sta_data.append({'steam_id_64': pid, 'rating_std': rating_std, 'win_rating': win_rating, 'loss_rating': loss_rating}) - df_sta = pd.DataFrame(sta_data) - - # --- Merge All --- - df = df_basic.merge(df_side_final, on='steam_id_64', how='left') - df = df.merge(df_hps, on='steam_id_64', how='left') - df = df.merge(df_ptl, on='steam_id_64', how='left').fillna(0) - df = df.merge(df_sta, on='steam_id_64', how='left') - - return df - -def normalize_series(series): - min_v = series.min() - max_v = series.max() - if pd.isna(min_v) or pd.isna(max_v) or min_v == max_v: - return pd.Series([50]*len(series), index=series.index) - return (series - min_v) / (max_v - min_v) * 100 - -def calculate_scores(df): - df = df.copy() - - # BAT - df['n_rating'] = normalize_series(df['avg_rating']) - df['n_kd'] = normalize_series(df['avg_kd']) - df['n_adr'] = normalize_series(df['avg_adr']) - df['n_kast'] = normalize_series(df['avg_kast']) - df['score_BAT'] = 0.4*df['n_rating'] + 0.3*df['n_kd'] + 0.2*df['n_adr'] + 0.1*df['n_kast'] - - # STA - df['n_std'] = normalize_series(df['rating_std'].fillna(0)) - df['n_win_r'] = normalize_series(df['win_rating'].fillna(0)) - df['n_loss_r'] = normalize_series(df['loss_rating'].fillna(0)) - df['score_STA'] = 0.5*(100 - df['n_std']) + 0.25*df['n_win_r'] + 0.25*df['n_loss_r'] - - # UTIL - df['n_util_dmg'] = normalize_series(df['total_util_dmg'] / df['matches_played']) - df['n_flash'] = normalize_series(df['total_flash_time'] / df['matches_played']) - df['score_UTIL'] = 0.6*df['n_util_dmg'] + 0.4*df['n_flash'] - - # T/CT (Calculated from Event Logs) - df['n_ct_kd'] = normalize_series(df['ct_kd'].fillna(0)) - df['n_t_kd'] = normalize_series(df['t_kd'].fillna(0)) - df['score_TCT'] = 0.5*df['n_ct_kd'] + 0.5*df['n_t_kd'] - - # HPS - df['n_clutch'] = normalize_series(df['total_clutches'] / df['matches_played']) - df['n_close_r'] = normalize_series(df['close_match_rating'].fillna(0)) - df['score_HPS'] = 0.5*df['n_clutch'] + 0.5*df['n_close_r'] - - # PTL - df['n_pistol'] = normalize_series(df['pistol_kills'] / df['matches_played']) - df['score_PTL'] = df['n_pistol'] - - return df - -def main(): - conn = get_db_connection() - try: - df = load_data_and_calculate(conn) - if df is None: return - - # Debug: Print raw stats for checking T/CT issue - print("\n--- Raw T/CT Stats Sample ---") - if 'ct_kd' in df.columns: - print(df[['steam_id_64', 'ct_kd', 't_kd']].head()) - else: - print("CT/KD columns missing") - - results = calculate_scores(df) - - print("\n--- Final Dimension Scores (Top 5 by BAT) ---") - cols = ['steam_id_64', 'score_BAT', 'score_STA', 'score_UTIL', 'score_TCT', 'score_HPS', 'score_PTL'] - print(results[cols].sort_values('score_BAT', ascending=False).head(5)) - - except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() - finally: - conn.close() - -if __name__ == "__main__": - main() diff --git a/scripts/analyze_l3_full.py b/scripts/analyze_l3_full.py deleted file mode 100644 index 41f4c09..0000000 --- a/scripts/analyze_l3_full.py +++ /dev/null @@ -1,304 +0,0 @@ -import sqlite3 -import pandas as pd -import numpy as np -import os - -DB_L2_PATH = r'd:\Documents\trae_projects\yrtv\database\L2\L2_Main.sqlite' - -def get_db_connection(): - conn = sqlite3.connect(DB_L2_PATH) - conn.row_factory = sqlite3.Row - return conn - -def load_comprehensive_data(conn, min_matches=5): - print("Loading Comprehensive Data...") - - # 1. Base Player List & Basic Stats - query_basic = """ - SELECT - steam_id_64, - COUNT(*) as total_matches, - AVG(rating) as basic_avg_rating, - AVG(kd_ratio) as basic_avg_kd, - AVG(adr) as basic_avg_adr, - AVG(kast) as basic_avg_kast, - AVG(rws) as basic_avg_rws, - SUM(headshot_count) as sum_headshot, - SUM(kills) as sum_kills, - SUM(deaths) as sum_deaths, - SUM(first_kill) as sum_fk, - SUM(first_death) as sum_fd, - SUM(kill_2) as sum_2k, - SUM(kill_3) as sum_3k, - SUM(kill_4) as sum_4k, - SUM(kill_5) as sum_5k, - SUM(assisted_kill) as sum_assist, - SUM(perfect_kill) as sum_perfect, - SUM(revenge_kill) as sum_revenge, - SUM(awp_kill) as sum_awp, - SUM(jump_count) as sum_jump, - SUM(clutch_1v1)+SUM(clutch_1v2)+SUM(clutch_1v3)+SUM(clutch_1v4)+SUM(clutch_1v5) as sum_clutches, - SUM(throw_harm) as sum_util_dmg, - SUM(flash_time) as sum_flash_time, - SUM(flash_enemy) as sum_flash_enemy, - SUM(flash_team) as sum_flash_team - FROM fact_match_players - GROUP BY steam_id_64 - HAVING COUNT(*) >= ? - """ - df = pd.read_sql_query(query_basic, conn, params=(min_matches,)) - - valid_ids = tuple(df['steam_id_64'].tolist()) - if not valid_ids: - print("No players found.") - return None - placeholders = ','.join(['?'] * len(valid_ids)) - - # --- Derived Basic Features --- - df['basic_headshot_rate'] = df['sum_headshot'] / df['sum_kills'].replace(0, 1) - df['basic_avg_headshot_kills'] = df['sum_headshot'] / df['total_matches'] - df['basic_avg_first_kill'] = df['sum_fk'] / df['total_matches'] - df['basic_avg_first_death'] = df['sum_fd'] / df['total_matches'] - df['basic_first_kill_rate'] = df['sum_fk'] / (df['sum_fk'] + df['sum_fd']).replace(0, 1) # Opening Success - df['basic_first_death_rate'] = df['sum_fd'] / (df['sum_fk'] + df['sum_fd']).replace(0, 1) - df['basic_avg_kill_2'] = df['sum_2k'] / df['total_matches'] - df['basic_avg_kill_3'] = df['sum_3k'] / df['total_matches'] - df['basic_avg_kill_4'] = df['sum_4k'] / df['total_matches'] - df['basic_avg_kill_5'] = df['sum_5k'] / df['total_matches'] - df['basic_avg_assisted_kill'] = df['sum_assist'] / df['total_matches'] - df['basic_avg_perfect_kill'] = df['sum_perfect'] / df['total_matches'] - df['basic_avg_revenge_kill'] = df['sum_revenge'] / df['total_matches'] - df['basic_avg_awp_kill'] = df['sum_awp'] / df['total_matches'] - df['basic_avg_jump_count'] = df['sum_jump'] / df['total_matches'] - - # 2. STA (Stability) - Detailed - print("Calculating STA...") - query_sta = f""" - SELECT mp.steam_id_64, mp.rating, mp.is_win, m.start_time - FROM fact_match_players mp - JOIN fact_matches m ON mp.match_id = m.match_id - WHERE mp.steam_id_64 IN ({placeholders}) - ORDER BY mp.steam_id_64, m.start_time - """ - df_matches = pd.read_sql_query(query_sta, conn, params=valid_ids) - - sta_list = [] - for pid, group in df_matches.groupby('steam_id_64'): - # Last 30 - last_30 = group.tail(30) - sta_last_30 = last_30['rating'].mean() - # Win/Loss - sta_win = group[group['is_win']==1]['rating'].mean() - sta_loss = group[group['is_win']==0]['rating'].mean() - # Volatility (Last 10) - sta_vol = group.tail(10)['rating'].std() - - # Time Decay (Simulated): Avg rating of 1st match of day vs >3rd match of day - # Need date conversion. - group['date'] = pd.to_datetime(group['start_time'], unit='s').dt.date - daily_counts = group.groupby('date').cumcount() - # Early: index 0, Late: index >= 2 - early_ratings = group[daily_counts == 0]['rating'] - late_ratings = group[daily_counts >= 2]['rating'] - - if len(late_ratings) > 0: - sta_fatigue = early_ratings.mean() - late_ratings.mean() # Positive means fatigue (drop) - else: - sta_fatigue = 0 - - sta_list.append({ - 'steam_id_64': pid, - 'sta_last_30_rating': sta_last_30, - 'sta_win_rating': sta_win, - 'sta_loss_rating': sta_loss, - 'sta_rating_volatility': sta_vol, - 'sta_fatigue_decay': sta_fatigue - }) - df_sta = pd.DataFrame(sta_list) - df = df.merge(df_sta, on='steam_id_64', how='left') - - # 3. BAT (Battle) - Detailed - print("Calculating BAT...") - # Need Match ELO - query_bat = f""" - SELECT mp.steam_id_64, mp.kd_ratio, mp.entry_kills, mp.entry_deaths, - (SELECT AVG(group_origin_elo) FROM fact_match_teams fmt WHERE fmt.match_id = mp.match_id AND group_origin_elo > 0) as match_elo - FROM fact_match_players mp - WHERE mp.steam_id_64 IN ({placeholders}) - """ - df_bat_raw = pd.read_sql_query(query_bat, conn, params=valid_ids) - - bat_list = [] - for pid, group in df_bat_raw.groupby('steam_id_64'): - avg_elo = group['match_elo'].mean() - if pd.isna(avg_elo): avg_elo = 1500 - - high_elo_kd = group[group['match_elo'] > avg_elo]['kd_ratio'].mean() - low_elo_kd = group[group['match_elo'] <= avg_elo]['kd_ratio'].mean() - - sum_entry_k = group['entry_kills'].sum() - sum_entry_d = group['entry_deaths'].sum() - duel_win_rate = sum_entry_k / (sum_entry_k + sum_entry_d) if (sum_entry_k+sum_entry_d) > 0 else 0 - - bat_list.append({ - 'steam_id_64': pid, - 'bat_kd_diff_high_elo': high_elo_kd, # Higher is better - 'bat_kd_diff_low_elo': low_elo_kd, - 'bat_avg_duel_win_rate': duel_win_rate - }) - df_bat = pd.DataFrame(bat_list) - df = df.merge(df_bat, on='steam_id_64', how='left') - - # 4. HPS (Pressure) - Detailed - print("Calculating HPS...") - # Complex query for Match Point and Pressure situations - # Logic: Round score diff. - # Since we don't have round-by-round player stats in L2 easily (economy table is sparse on stats), - # We use Matches for "Close Match" and "Comeback" - - # Comeback/Close Match Logic on MATCH level - query_hps_match = f""" - SELECT mp.steam_id_64, mp.kd_ratio, mp.rating, m.score_team1, m.score_team2, mp.team_id, m.winner_team - FROM fact_match_players mp - JOIN fact_matches m ON mp.match_id = m.match_id - WHERE mp.steam_id_64 IN ({placeholders}) - """ - df_hps_raw = pd.read_sql_query(query_hps_match, conn, params=valid_ids) - - hps_list = [] - for pid, group in df_hps_raw.groupby('steam_id_64'): - # Close Match: Score diff <= 3 - group['score_diff'] = abs(group['score_team1'] - group['score_team2']) - close_rating = group[group['score_diff'] <= 3]['rating'].mean() - - # Comeback: Won match where score was close? - # Actually without round history, we can't define "Comeback" (was behind then won). - # We can define "Underdog Win": Won when ELO was lower? Or just Close Win. - # Let's use Close Match Rating as primary HPS metric from matches. - - hps_list.append({ - 'steam_id_64': pid, - 'hps_close_match_rating': close_rating - }) - df_hps = pd.DataFrame(hps_list) - - # HPS Clutch (from Basic) - df['hps_clutch_rate'] = df['sum_clutches'] / df['total_matches'] - - df = df.merge(df_hps, on='steam_id_64', how='left') - - # 5. PTL (Pistol) - print("Calculating PTL...") - # R1/R13 Kills - query_ptl = f""" - SELECT ev.attacker_steam_id as steam_id_64, COUNT(*) as pistol_kills - FROM fact_round_events ev - WHERE ev.event_type = 'kill' AND ev.round_num IN (1, 13) - AND ev.attacker_steam_id IN ({placeholders}) - GROUP BY ev.attacker_steam_id - """ - df_ptl = pd.read_sql_query(query_ptl, conn, params=valid_ids) - # Pistol Win Rate (Team) - # Need to join rounds. Too slow? - # Simplify: Just use Pistol Kills per Match (normalized) - - df = df.merge(df_ptl, on='steam_id_64', how='left') - df['ptl_pistol_kills_per_match'] = df['pistol_kills'] / df['total_matches'] - - # 6. T/CT - print("Calculating T/CT...") - query_ct = f"SELECT steam_id_64, AVG(rating) as ct_rating, AVG(kd_ratio) as ct_kd FROM fact_match_players_ct WHERE steam_id_64 IN ({placeholders}) GROUP BY steam_id_64" - query_t = f"SELECT steam_id_64, AVG(rating) as t_rating, AVG(kd_ratio) as t_kd FROM fact_match_players_t WHERE steam_id_64 IN ({placeholders}) GROUP BY steam_id_64" - df_ct = pd.read_sql_query(query_ct, conn, params=valid_ids) - df_t = pd.read_sql_query(query_t, conn, params=valid_ids) - df = df.merge(df_ct, on='steam_id_64', how='left').merge(df_t, on='steam_id_64', how='left') - - # 7. UTIL - print("Calculating UTIL...") - df['util_avg_dmg'] = df['sum_util_dmg'] / df['total_matches'] - df['util_avg_flash_time'] = df['sum_flash_time'] / df['total_matches'] - - return df - -def normalize(series): - s = series.fillna(series.mean()) - if s.max() == s.min(): return pd.Series([50]*len(s), index=s.index) - return (s - s.min()) / (s.max() - s.min()) * 100 - -def calculate_full_scores(df): - df = df.copy() - - # --- BAT Calculation --- - # Components: Rating, KD, ADR, KAST, Duel Win Rate, High ELO KD - # Weights: Rating(30), KD(20), ADR(15), KAST(10), Duel(15), HighELO(10) - df['n_bat_rating'] = normalize(df['basic_avg_rating']) - df['n_bat_kd'] = normalize(df['basic_avg_kd']) - df['n_bat_adr'] = normalize(df['basic_avg_adr']) - df['n_bat_kast'] = normalize(df['basic_avg_kast']) - df['n_bat_duel'] = normalize(df['bat_avg_duel_win_rate']) - df['n_bat_high'] = normalize(df['bat_kd_diff_high_elo']) - - df['score_BAT'] = (0.3*df['n_bat_rating'] + 0.2*df['n_bat_kd'] + 0.15*df['n_bat_adr'] + - 0.1*df['n_bat_kast'] + 0.15*df['n_bat_duel'] + 0.1*df['n_bat_high']) - - # --- STA Calculation --- - # Components: Volatility (Neg), Win Rating, Loss Rating, Fatigue (Neg) - # Weights: Consistency(40), WinPerf(20), LossPerf(30), Fatigue(10) - df['n_sta_vol'] = normalize(df['sta_rating_volatility']) # Lower is better -> 100 - X - df['n_sta_win'] = normalize(df['sta_win_rating']) - df['n_sta_loss'] = normalize(df['sta_loss_rating']) - df['n_sta_fat'] = normalize(df['sta_fatigue_decay']) # Lower (less drop) is better -> 100 - X - - df['score_STA'] = (0.4*(100-df['n_sta_vol']) + 0.2*df['n_sta_win'] + - 0.3*df['n_sta_loss'] + 0.1*(100-df['n_sta_fat'])) - - # --- HPS Calculation --- - # Components: Clutch Rate, Close Match Rating - df['n_hps_clutch'] = normalize(df['hps_clutch_rate']) - df['n_hps_close'] = normalize(df['hps_close_match_rating']) - - df['score_HPS'] = 0.5*df['n_hps_clutch'] + 0.5*df['n_hps_close'] - - # --- PTL Calculation --- - # Components: Pistol Kills/Match - df['score_PTL'] = normalize(df['ptl_pistol_kills_per_match']) - - # --- T/CT Calculation --- - # Components: CT Rating, T Rating - df['n_ct'] = normalize(df['ct_rating']) - df['n_t'] = normalize(df['t_rating']) - df['score_TCT'] = 0.5*df['n_ct'] + 0.5*df['n_t'] - - # --- UTIL Calculation --- - # Components: Dmg, Flash Time - df['n_util_dmg'] = normalize(df['util_avg_dmg']) - df['n_util_flash'] = normalize(df['util_avg_flash_time']) - df['score_UTIL'] = 0.6*df['n_util_dmg'] + 0.4*df['n_util_flash'] - - return df - -def main(): - conn = get_db_connection() - try: - df = load_comprehensive_data(conn) - if df is None: return - - results = calculate_full_scores(df) - - print("\n--- Final Full Scores ---") - cols = ['steam_id_64', 'score_BAT', 'score_STA', 'score_UTIL', 'score_TCT', 'score_HPS', 'score_PTL'] - print(results[cols].sort_values('score_BAT', ascending=False).head(5)) - - print("\n--- Available Features Used ---") - print("BAT: Rating, KD, ADR, KAST, Duel Win Rate, High ELO Performance") - print("STA: Volatility, Win Rating, Loss Rating, Fatigue Decay") - print("HPS: Clutch Rate, Close Match Rating") - print("PTL: Pistol Kills per Match") - print("T/CT: CT Rating, T Rating") - print("UTIL: Util Dmg, Flash Duration") - - finally: - conn.close() - -if __name__ == "__main__": - main() diff --git a/scripts/analyze_l3_ultimate.py b/scripts/analyze_l3_ultimate.py deleted file mode 100644 index f2c2a43..0000000 --- a/scripts/analyze_l3_ultimate.py +++ /dev/null @@ -1,499 +0,0 @@ -import sqlite3 -import pandas as pd -import numpy as np -import os - -DB_L2_PATH = r'd:\Documents\trae_projects\yrtv\database\L2\L2_Main.sqlite' - -def get_db_connection(): - conn = sqlite3.connect(DB_L2_PATH) - conn.row_factory = sqlite3.Row - return conn - -def safe_div(a, b): - if b == 0: return 0 - return a / b - -def load_and_calculate_ultimate(conn, min_matches=5): - print("Loading Ultimate Data Set...") - - # 1. Basic Stats (Already have) - query_basic = """ - SELECT - steam_id_64, - COUNT(*) as matches_played, - SUM(round_total) as rounds_played, - AVG(rating) as basic_avg_rating, - AVG(kd_ratio) as basic_avg_kd, - AVG(adr) as basic_avg_adr, - AVG(kast) as basic_avg_kast, - AVG(rws) as basic_avg_rws, - SUM(headshot_count) as sum_hs, - SUM(kills) as sum_kills, - SUM(deaths) as sum_deaths, - SUM(first_kill) as sum_fk, - SUM(first_death) as sum_fd, - SUM(clutch_1v1) as sum_1v1, - SUM(clutch_1v2) as sum_1v2, - SUM(clutch_1v3) + SUM(clutch_1v4) + SUM(clutch_1v5) as sum_1v3p, - SUM(kill_2) as sum_2k, - SUM(kill_3) as sum_3k, - SUM(kill_4) as sum_4k, - SUM(kill_5) as sum_5k, - SUM(assisted_kill) as sum_assist, - SUM(perfect_kill) as sum_perfect, - SUM(revenge_kill) as sum_revenge, - SUM(awp_kill) as sum_awp, - SUM(jump_count) as sum_jump, - SUM(throw_harm) as sum_util_dmg, - SUM(flash_time) as sum_flash_time, - SUM(flash_enemy) as sum_flash_enemy, - SUM(flash_team) as sum_flash_team - FROM fact_match_players - GROUP BY steam_id_64 - HAVING COUNT(*) >= ? - """ - df = pd.read_sql_query(query_basic, conn, params=(min_matches,)) - valid_ids = tuple(df['steam_id_64'].tolist()) - if not valid_ids: return None - placeholders = ','.join(['?'] * len(valid_ids)) - - # --- Basic Derived --- - df['basic_headshot_rate'] = df['sum_hs'] / df['sum_kills'].replace(0, 1) - df['basic_avg_headshot_kills'] = df['sum_hs'] / df['matches_played'] - df['basic_avg_first_kill'] = df['sum_fk'] / df['matches_played'] - df['basic_avg_first_death'] = df['sum_fd'] / df['matches_played'] - df['basic_first_kill_rate'] = df['sum_fk'] / (df['sum_fk'] + df['sum_fd']).replace(0, 1) - df['basic_first_death_rate'] = df['sum_fd'] / (df['sum_fk'] + df['sum_fd']).replace(0, 1) - df['basic_avg_kill_2'] = df['sum_2k'] / df['matches_played'] - df['basic_avg_kill_3'] = df['sum_3k'] / df['matches_played'] - df['basic_avg_kill_4'] = df['sum_4k'] / df['matches_played'] - df['basic_avg_kill_5'] = df['sum_5k'] / df['matches_played'] - df['basic_avg_assisted_kill'] = df['sum_assist'] / df['matches_played'] - df['basic_avg_perfect_kill'] = df['sum_perfect'] / df['matches_played'] - df['basic_avg_revenge_kill'] = df['sum_revenge'] / df['matches_played'] - df['basic_avg_awp_kill'] = df['sum_awp'] / df['matches_played'] - df['basic_avg_jump_count'] = df['sum_jump'] / df['matches_played'] - - # 2. STA - Detailed Time Series - print("Calculating STA (Detailed)...") - query_sta = f""" - SELECT mp.steam_id_64, mp.rating, mp.is_win, m.start_time, m.duration - FROM fact_match_players mp - JOIN fact_matches m ON mp.match_id = m.match_id - WHERE mp.steam_id_64 IN ({placeholders}) - ORDER BY mp.steam_id_64, m.start_time - """ - df_matches = pd.read_sql_query(query_sta, conn, params=valid_ids) - - sta_list = [] - for pid, group in df_matches.groupby('steam_id_64'): - group = group.sort_values('start_time') - # Last 30 - last_30 = group.tail(30) - sta_last_30 = last_30['rating'].mean() - # Win/Loss - sta_win = group[group['is_win']==1]['rating'].mean() - sta_loss = group[group['is_win']==0]['rating'].mean() - # Volatility - sta_vol = group.tail(10)['rating'].std() - # Time Correlation (Duration vs Rating) - sta_time_corr = group['duration'].corr(group['rating']) if len(group) > 2 else 0 - # Fatigue - group['date'] = pd.to_datetime(group['start_time'], unit='s').dt.date - daily = group.groupby('date')['rating'].agg(['first', 'last', 'count']) - daily_fatigue = daily[daily['count'] >= 3] - if len(daily_fatigue) > 0: - fatigue_decay = (daily_fatigue['first'] - daily_fatigue['last']).mean() - else: - fatigue_decay = 0 - - sta_list.append({ - 'steam_id_64': pid, - 'sta_last_30_rating': sta_last_30, - 'sta_win_rating': sta_win, - 'sta_loss_rating': sta_loss, - 'sta_rating_volatility': sta_vol, - 'sta_time_rating_corr': sta_time_corr, - 'sta_fatigue_decay': fatigue_decay - }) - df = df.merge(pd.DataFrame(sta_list), on='steam_id_64', how='left') - - # 3. BAT - Distance & Advanced - print("Calculating BAT (Distance & Context)...") - # Distance Logic: Get all kills with positions - # We need to map positions. - query_dist = f""" - SELECT attacker_steam_id as steam_id_64, - attacker_pos_x, attacker_pos_y, attacker_pos_z, - victim_pos_x, victim_pos_y, victim_pos_z - FROM fact_round_events - WHERE event_type = 'kill' - AND attacker_steam_id IN ({placeholders}) - AND attacker_pos_x IS NOT NULL AND victim_pos_x IS NOT NULL - """ - # Note: This might be heavy. If memory issue, sample or chunk. - try: - df_dist = pd.read_sql_query(query_dist, conn, params=valid_ids) - if not df_dist.empty: - # Calc Euclidian Distance - df_dist['dist'] = np.sqrt( - (df_dist['attacker_pos_x'] - df_dist['victim_pos_x'])**2 + - (df_dist['attacker_pos_y'] - df_dist['victim_pos_y'])**2 + - (df_dist['attacker_pos_z'] - df_dist['victim_pos_z'])**2 - ) - # Units: 1 unit ~ 1 inch. - # Close: < 500 (~12m) - # Mid: 500 - 1500 (~12m - 38m) - # Far: > 1500 - df_dist['is_close'] = df_dist['dist'] < 500 - df_dist['is_mid'] = (df_dist['dist'] >= 500) & (df_dist['dist'] <= 1500) - df_dist['is_far'] = df_dist['dist'] > 1500 - - bat_dist = df_dist.groupby('steam_id_64').agg({ - 'is_close': 'mean', # % of kills that are close - 'is_mid': 'mean', - 'is_far': 'mean' - }).reset_index() - bat_dist.columns = ['steam_id_64', 'bat_kill_share_close', 'bat_kill_share_mid', 'bat_kill_share_far'] - - # Note: "Win Rate" by distance requires Deaths by distance. - # We can try to get deaths too, but for now Share of Kills is a good proxy for "Preference/Style" - # To get "Win Rate", we need to know how many duels occurred at that distance. - # Approximation: Win Rate = Kills_at_dist / (Kills_at_dist + Deaths_at_dist) - - # Fetch Deaths - query_dist_d = f""" - SELECT victim_steam_id as steam_id_64, - attacker_pos_x, attacker_pos_y, attacker_pos_z, - victim_pos_x, victim_pos_y, victim_pos_z - FROM fact_round_events - WHERE event_type = 'kill' - AND victim_steam_id IN ({placeholders}) - AND attacker_pos_x IS NOT NULL AND victim_pos_x IS NOT NULL - """ - df_dist_d = pd.read_sql_query(query_dist_d, conn, params=valid_ids) - df_dist_d['dist'] = np.sqrt( - (df_dist_d['attacker_pos_x'] - df_dist_d['victim_pos_x'])**2 + - (df_dist_d['attacker_pos_y'] - df_dist_d['victim_pos_y'])**2 + - (df_dist_d['attacker_pos_z'] - df_dist_d['victim_pos_z'])**2 - ) - - # Aggregate Kills Counts - k_counts = df_dist.groupby('steam_id_64').agg( - k_close=('is_close', 'sum'), - k_mid=('is_mid', 'sum'), - k_far=('is_far', 'sum') - ) - # Aggregate Deaths Counts - df_dist_d['is_close'] = df_dist_d['dist'] < 500 - df_dist_d['is_mid'] = (df_dist_d['dist'] >= 500) & (df_dist_d['dist'] <= 1500) - df_dist_d['is_far'] = df_dist_d['dist'] > 1500 - d_counts = df_dist_d.groupby('steam_id_64').agg( - d_close=('is_close', 'sum'), - d_mid=('is_mid', 'sum'), - d_far=('is_far', 'sum') - ) - - # Merge - bat_rates = k_counts.join(d_counts, how='outer').fillna(0) - bat_rates['bat_win_rate_close'] = bat_rates['k_close'] / (bat_rates['k_close'] + bat_rates['d_close']).replace(0, 1) - bat_rates['bat_win_rate_mid'] = bat_rates['k_mid'] / (bat_rates['k_mid'] + bat_rates['d_mid']).replace(0, 1) - bat_rates['bat_win_rate_far'] = bat_rates['k_far'] / (bat_rates['k_far'] + bat_rates['d_far']).replace(0, 1) - bat_rates['bat_win_rate_vs_all'] = (bat_rates['k_close']+bat_rates['k_mid']+bat_rates['k_far']) / (bat_rates['k_close']+bat_rates['d_close']+bat_rates['k_mid']+bat_rates['d_mid']+bat_rates['k_far']+bat_rates['d_far']).replace(0, 1) - - df = df.merge(bat_rates[['bat_win_rate_close', 'bat_win_rate_mid', 'bat_win_rate_far', 'bat_win_rate_vs_all']], on='steam_id_64', how='left') - else: - print("No position data found.") - except Exception as e: - print(f"Dist calculation error: {e}") - - # High/Low ELO KD - query_elo = f""" - SELECT mp.steam_id_64, mp.kd_ratio, - (SELECT AVG(group_origin_elo) FROM fact_match_teams fmt WHERE fmt.match_id = mp.match_id AND group_origin_elo > 0) as elo - FROM fact_match_players mp - WHERE mp.steam_id_64 IN ({placeholders}) - """ - df_elo = pd.read_sql_query(query_elo, conn, params=valid_ids) - elo_list = [] - for pid, group in df_elo.groupby('steam_id_64'): - avg = group['elo'].mean() - if pd.isna(avg): avg = 1000 - elo_list.append({ - 'steam_id_64': pid, - 'bat_kd_diff_high_elo': group[group['elo'] > avg]['kd_ratio'].mean(), - 'bat_kd_diff_low_elo': group[group['elo'] <= avg]['kd_ratio'].mean() - }) - df = df.merge(pd.DataFrame(elo_list), on='steam_id_64', how='left') - - # Avg Duel Freq - df['bat_avg_duel_freq'] = (df['sum_fk'] + df['sum_fd']) / df['rounds_played'] - - # 4. HPS - High Pressure Contexts - print("Calculating HPS (Contexts)...") - # We need round-by-round score evolution. - # Join rounds and economy(side) and matches - query_hps_ctx = f""" - SELECT r.match_id, r.round_num, r.ct_score, r.t_score, r.winner_side, - m.score_team1, m.score_team2, m.winner_team, - e.steam_id_64, e.side as player_side, - (SELECT COUNT(*) FROM fact_round_events ev WHERE ev.match_id=r.match_id AND ev.round_num=r.round_num AND ev.attacker_steam_id=e.steam_id_64 AND ev.event_type='kill') as kills, - (SELECT COUNT(*) FROM fact_round_events ev WHERE ev.match_id=r.match_id AND ev.round_num=r.round_num AND ev.victim_steam_id=e.steam_id_64 AND ev.event_type='kill') as deaths - FROM fact_rounds r - JOIN fact_matches m ON r.match_id = m.match_id - JOIN fact_round_player_economy e ON r.match_id = e.match_id AND r.round_num = e.round_num - WHERE e.steam_id_64 IN ({placeholders}) - """ - # This is heavy. - try: - # Optimization: Process per match or use SQL aggregation? - # SQL aggregation for specific conditions is better. - - # 4.1 Match Point Win Rate - # Condition: (player_side='CT' AND ct_score >= 12) OR (player_side='T' AND t_score >= 12) (Assuming MR12) - # Or just max score of match? - # Let's approximate: Rounds where total_score >= 23 (MR12) or 29 (MR15) - # Actually, let's use: round_num >= match.round_total - 1? No. - # Use: Rounds where One Team Score = Match Win Score - 1. - # Since we don't know MR12/MR15 per match easily (some are short), check `game_mode`. - # Fallback: Rounds where `ct_score` or `t_score` >= 12. - - # 4.2 Pressure Entry Rate (Losing Streak) - # Condition: Team score < Enemy score - 3. - - # 4.3 Momentum Multi-kill (Winning Streak) - # Condition: Team score > Enemy score + 3. - - # Let's load a simplified dataframe of rounds - df_rounds = pd.read_sql_query(query_hps_ctx, conn, params=valid_ids) - - hps_stats = [] - for pid, group in df_rounds.groupby('steam_id_64'): - # Determine Player Team Score and Enemy Team Score - # If player_side == 'CT', player_score = ct_score - group['my_score'] = np.where(group['player_side'] == 'CT', group['ct_score'], group['t_score']) - group['enemy_score'] = np.where(group['player_side'] == 'CT', group['t_score'], group['ct_score']) - - # Match Point (My team or Enemy team at match point) - # Simple heuristic: Score >= 12 - is_match_point = (group['my_score'] >= 12) | (group['enemy_score'] >= 12) - mp_rounds = group[is_match_point] - # Did we win? - # winner_side matches player_side - mp_wins = mp_rounds[mp_rounds['winner_side'] == mp_rounds['player_side']] - mp_win_rate = len(mp_wins) / len(mp_rounds) if len(mp_rounds) > 0 else 0.5 - - # Pressure (Losing by 3+) - is_pressure = (group['enemy_score'] - group['my_score']) >= 3 - # Entry Rate in pressure? Need FK data. - # We only loaded kills. Let's use Kills per round in pressure. - pressure_kpr = group[is_pressure]['kills'].mean() if len(group[is_pressure]) > 0 else 0 - - # Momentum (Winning by 3+) - is_momentum = (group['my_score'] - group['enemy_score']) >= 3 - # Multi-kill rate (>=2 kills) - momentum_rounds = group[is_momentum] - momentum_multikills = len(momentum_rounds[momentum_rounds['kills'] >= 2]) - momentum_mk_rate = momentum_multikills / len(momentum_rounds) if len(momentum_rounds) > 0 else 0 - - # Comeback KD Diff - # Avg KD in Pressure rounds vs Avg KD overall - pressure_deaths = group[is_pressure]['deaths'].sum() - pressure_kills = group[is_pressure]['kills'].sum() - pressure_kd = pressure_kills / pressure_deaths if pressure_deaths > 0 else pressure_kills - - overall_deaths = group['deaths'].sum() - overall_kills = group['kills'].sum() - overall_kd = overall_kills / overall_deaths if overall_deaths > 0 else overall_kills - - comeback_diff = pressure_kd - overall_kd - - hps_stats.append({ - 'steam_id_64': pid, - 'hps_match_point_win_rate': mp_win_rate, - 'hps_pressure_entry_rate': pressure_kpr, # Proxy - 'hps_momentum_multikill_rate': momentum_mk_rate, - 'hps_comeback_kd_diff': comeback_diff, - 'hps_losing_streak_kd_diff': comeback_diff # Same metric - }) - - df = df.merge(pd.DataFrame(hps_stats), on='steam_id_64', how='left') - - # 4.4 Clutch Win Rates (Detailed) - df['hps_clutch_win_rate_1v1'] = df['sum_1v1'] / df['matches_played'] # Normalizing by match for now, ideal is by 1v1 opportunities - df['hps_clutch_win_rate_1v2'] = df['sum_1v2'] / df['matches_played'] - df['hps_clutch_win_rate_1v3_plus'] = df['sum_1v3p'] / df['matches_played'] - - # 4.5 Close Match Rating (from previous) - # ... (Already have logic in previous script, reusing) - - except Exception as e: - print(f"HPS Error: {e}") - - # 5. PTL - Pistol Detailed - print("Calculating PTL...") - # Filter Round 1, 13 (and 16 for MR15?) - # Just use 1 and 13 (common for MR12) - query_ptl = f""" - SELECT - e.steam_id_64, - (SELECT COUNT(*) FROM fact_round_events ev WHERE ev.match_id=e.match_id AND ev.round_num=e.round_num AND ev.attacker_steam_id=e.steam_id_64 AND ev.event_type='kill') as kills, - (SELECT COUNT(*) FROM fact_round_events ev WHERE ev.match_id=e.match_id AND ev.round_num=e.round_num AND ev.victim_steam_id=e.steam_id_64 AND ev.event_type='kill') as deaths, - r.winner_side, e.side as player_side, - e.equipment_value - FROM fact_round_player_economy e - JOIN fact_rounds r ON e.match_id = r.match_id AND e.round_num = r.round_num - WHERE e.steam_id_64 IN ({placeholders}) - AND e.round_num IN (1, 13) - """ - try: - df_ptl_raw = pd.read_sql_query(query_ptl, conn, params=valid_ids) - ptl_stats = [] - for pid, group in df_ptl_raw.groupby('steam_id_64'): - kills = group['kills'].sum() - deaths = group['deaths'].sum() - kd = kills / deaths if deaths > 0 else kills - - wins = len(group[group['winner_side'] == group['player_side']]) - win_rate = wins / len(group) - - multikills = len(group[group['kills'] >= 2]) - - # Util Efficiency: Not easy here. - - ptl_stats.append({ - 'steam_id_64': pid, - 'ptl_pistol_kills': kills, # Total? Or Avg? Schema says REAL. Let's use Avg per Match later. - 'ptl_pistol_kd': kd, - 'ptl_pistol_win_rate': win_rate, - 'ptl_pistol_multikills': multikills - }) - - df_ptl = pd.DataFrame(ptl_stats) - df_ptl['ptl_pistol_kills'] = df_ptl['ptl_pistol_kills'] / df['matches_played'].mean() # Approximate - df = df.merge(df_ptl, on='steam_id_64', how='left') - - except Exception as e: - print(f"PTL Error: {e}") - - # 6. T/CT & UTIL (Straightforward) - print("Calculating T/CT & UTIL...") - # T/CT Side Stats - query_side = f""" - SELECT steam_id_64, - SUM(CASE WHEN side='CT' THEN 1 ELSE 0 END) as ct_rounds, - SUM(CASE WHEN side='T' THEN 1 ELSE 0 END) as t_rounds - FROM fact_round_player_economy - WHERE steam_id_64 IN ({placeholders}) - GROUP BY steam_id_64 - """ - # Combine with aggregated ratings from fact_match_players_ct/t - query_side_r = f""" - SELECT steam_id_64, AVG(rating) as ct_rating, AVG(kd_ratio) as ct_kd, SUM(first_kill) as ct_fk - FROM fact_match_players_ct WHERE steam_id_64 IN ({placeholders}) GROUP BY steam_id_64 - """ - df_ct = pd.read_sql_query(query_side_r, conn, params=valid_ids) - # Similar for T... - - # Merge... - - # UTIL - df['util_avg_nade_dmg'] = df['sum_util_dmg'] / df['matches_played'] - df['util_avg_flash_time'] = df['sum_flash_time'] / df['matches_played'] - df['util_avg_flash_enemy'] = df['sum_flash_enemy'] / df['matches_played'] - - # Fill NaN - df = df.fillna(0) - - return df - -def calculate_ultimate_scores(df): - # Normalize Helper - def n(col): - if col not in df.columns: return 50 - s = df[col] - if s.max() == s.min(): return 50 - return (s - s.min()) / (s.max() - s.min()) * 100 - - df = df.copy() - - # 1. BAT: Battle (30%) - # Weights: Rating(25), KD(20), ADR(15), Duel(10), HighELO(10), CloseRange(10), MultiKill(10) - df['score_BAT'] = ( - 0.25 * n('basic_avg_rating') + - 0.20 * n('basic_avg_kd') + - 0.15 * n('basic_avg_adr') + - 0.10 * n('bat_avg_duel_win_rate') + # Need to ensure col exists - 0.10 * n('bat_kd_diff_high_elo') + - 0.10 * n('bat_win_rate_close') + - 0.10 * n('basic_avg_kill_3') # Multi-kill proxy - ) - - # 2. STA: Stability (15%) - # Weights: Volatility(30), LossRating(30), WinRating(20), TimeCorr(10), Fatigue(10) - df['score_STA'] = ( - 0.30 * (100 - n('sta_rating_volatility')) + - 0.30 * n('sta_loss_rating') + - 0.20 * n('sta_win_rating') + - 0.10 * (100 - n('sta_time_rating_corr').abs()) + # Closer to 0 is better (independent of duration) - 0.10 * (100 - n('sta_fatigue_decay')) - ) - - # 3. HPS: Pressure (20%) - # Weights: Clutch(30), MatchPoint(20), Comeback(20), PressureEntry(15), CloseMatch(15) - df['score_HPS'] = ( - 0.30 * n('sum_1v3p') + # Using high tier clutches - 0.20 * n('hps_match_point_win_rate') + - 0.20 * n('hps_comeback_kd_diff') + - 0.15 * n('hps_pressure_entry_rate') + - 0.15 * n('basic_avg_rating') # Fallback if close match rating missing - ) - - # 4. PTL: Pistol (10%) - # Weights: Kills(40), WinRate(30), KD(30) - df['score_PTL'] = ( - 0.40 * n('ptl_pistol_kills') + - 0.30 * n('ptl_pistol_win_rate') + - 0.30 * n('ptl_pistol_kd') - ) - - # 5. T/CT (15%) - # Weights: CT(50), T(50) - # Need to load CT/T ratings properly, using basic rating as placeholder if missing - df['score_TCT'] = 0.5 * n('basic_avg_rating') + 0.5 * n('basic_avg_rating') - - # 6. UTIL (10%) - # Weights: Dmg(50), Flash(30), EnemiesFlashed(20) - df['score_UTIL'] = ( - 0.50 * n('util_avg_nade_dmg') + - 0.30 * n('util_avg_flash_time') + - 0.20 * n('util_avg_flash_enemy') - ) - - return df - -def main(): - conn = get_db_connection() - try: - df = load_and_calculate_ultimate(conn) - if df is None: return - - results = calculate_ultimate_scores(df) - - print("\n--- Ultimate Scores (Top 5 BAT) ---") - cols = ['steam_id_64', 'score_BAT', 'score_STA', 'score_HPS', 'score_PTL', 'score_UTIL'] - print(results[cols].sort_values('score_BAT', ascending=False).head(5)) - - # Verify coverage - print("\n--- Feature Coverage ---") - print(f"Total Columns: {len(results.columns)}") - print("BAT Distances:", 'bat_win_rate_close' in results.columns) - print("HPS Contexts:", 'hps_match_point_win_rate' in results.columns) - print("PTL Detailed:", 'ptl_pistol_kd' in results.columns) - - finally: - conn.close() - -if __name__ == "__main__": - main() diff --git a/scripts/check_l1a.py b/scripts/check_l1a.py deleted file mode 100644 index 137c038..0000000 --- a/scripts/check_l1a.py +++ /dev/null @@ -1,22 +0,0 @@ -import sqlite3 -import os - -L1A_DB_PATH = r'd:\Documents\trae_projects\yrtv\database\L1A\L1A.sqlite' - -print("Checking L1A...") -if os.path.exists(L1A_DB_PATH): - try: - conn = sqlite3.connect(L1A_DB_PATH) - cursor = conn.cursor() - cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") - tables = cursor.fetchall() - print(f"Tables: {tables}") - - cursor.execute("SELECT COUNT(*) FROM raw_iframe_network") - count = cursor.fetchone()[0] - print(f"L1A Records: {count}") - conn.close() - except Exception as e: - print(f"Error checking L1A: {e}") -else: - print(f"L1A DB not found at {L1A_DB_PATH}") diff --git a/scripts/check_l3_final.py b/scripts/check_l3_final.py deleted file mode 100644 index 9815166..0000000 --- a/scripts/check_l3_final.py +++ /dev/null @@ -1,19 +0,0 @@ -import sqlite3 -import pandas as pd -import os - -db_path = r'd:\Documents\trae_projects\yrtv\database\L3\L3_Features.sqlite' -conn = sqlite3.connect(db_path) -try: - print("Checking L3 Obj and KAST:") - df = pd.read_sql_query(""" - SELECT - steam_id_64, - side_obj_t, side_obj_ct, - side_kast_t, side_kast_ct - FROM dm_player_features - LIMIT 5 - """, conn) - print(df) -finally: - conn.close() diff --git a/scripts/check_l3_variance.py b/scripts/check_l3_variance.py deleted file mode 100644 index 5bb825c..0000000 --- a/scripts/check_l3_variance.py +++ /dev/null @@ -1,55 +0,0 @@ -import sqlite3 -import pandas as pd -import numpy as np -import os - -# Config to match your project structure -class Config: - DB_L3_PATH = r'd:\Documents\trae_projects\yrtv\database\L3\L3_Features.sqlite' - -def check_variance(): - db_path = Config.DB_L3_PATH - if not os.path.exists(db_path): - print(f"L3 DB not found at {db_path}") - return - - conn = sqlite3.connect(db_path) - try: - # Read all features - df = pd.read_sql_query("SELECT * FROM dm_player_features", conn) - - print(f"Total rows: {len(df)}") - if len(df) == 0: - print("Table is empty.") - return - - numeric_cols = df.select_dtypes(include=['number']).columns - - print("\n--- Variance Analysis ---") - for col in numeric_cols: - if col in ['steam_id_64']: continue # Skip ID - - # Check for all zeros - if (df[col] == 0).all(): - print(f"[ALL ZERO] {col}") - continue - - # Check for single value (variance = 0) - if df[col].nunique() <= 1: - val = df[col].iloc[0] - print(f"[SINGLE VAL] {col} = {val}") - continue - - # Check for mostly zeros - zero_pct = (df[col] == 0).mean() - if zero_pct > 0.9: - print(f"[MOSTLY ZERO] {col} ({zero_pct:.1%} zeros)") - - # Basic stats for valid ones - # print(f"{col}: min={df[col].min():.2f}, max={df[col].max():.2f}, mean={df[col].mean():.2f}") - - finally: - conn.close() - -if __name__ == "__main__": - check_variance() diff --git a/scripts/check_round_data.py b/scripts/check_round_data.py deleted file mode 100644 index 658f545..0000000 --- a/scripts/check_round_data.py +++ /dev/null @@ -1,45 +0,0 @@ - -import sqlite3 -import pandas as pd - -match_id = 'g161-n-20251222204652101389654' - -def check_data(): - conn = sqlite3.connect('database/L2/L2_Main.sqlite') - - print(f"--- Check Match: {match_id} ---") - - # 1. Source Type - c = conn.cursor() - c.execute("SELECT data_source_type FROM fact_matches WHERE match_id = ?", (match_id,)) - row = c.fetchone() - if row: - print(f"Data Source: {row[0]}") - else: - print("Match not found") - return - - # 2. Round Events (Sample) - print("\n--- Round Events Sample ---") - try: - df = pd.read_sql(f"SELECT round_num, event_type, attacker_steam_id, victim_steam_id, weapon FROM fact_round_events WHERE match_id = '{match_id}' LIMIT 5", conn) - print(df) - if df.empty: - print("WARNING: No events found.") - except Exception as e: - print(e) - - # 3. Economy (Sample) - print("\n--- Economy Sample ---") - try: - df_eco = pd.read_sql(f"SELECT round_num, steam_id_64, equipment_value FROM fact_round_player_economy WHERE match_id = '{match_id}' LIMIT 5", conn) - print(df_eco) - if df_eco.empty: - print("Info: No economy data (Likely Classic source).") - except Exception as e: - print(e) - - conn.close() - -if __name__ == "__main__": - check_data() diff --git a/scripts/check_side_mapping.py b/scripts/check_side_mapping.py deleted file mode 100644 index 1d72ffe..0000000 --- a/scripts/check_side_mapping.py +++ /dev/null @@ -1,63 +0,0 @@ -import sqlite3 -import pandas as pd -import json -import os -import sys - -# Add parent directory -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from web.config import Config - -def check_mapping(): - conn = sqlite3.connect(Config.DB_L2_PATH) - - # Join economy and teams via match_id - # We need to match steam_id (in eco) to group_uids (in teams) - - # 1. Get Economy R1 samples - query_eco = """ - SELECT match_id, steam_id_64, side - FROM fact_round_player_economy - WHERE round_num = 1 - LIMIT 10 - """ - eco_rows = pd.read_sql_query(query_eco, conn) - - if eco_rows.empty: - print("No Economy R1 data found.") - conn.close() - return - - print("Checking Mapping...") - for _, row in eco_rows.iterrows(): - mid = row['match_id'] - sid = row['steam_id_64'] - side = row['side'] - - # Get Teams for this match - query_teams = "SELECT group_id, group_fh_role, group_uids FROM fact_match_teams WHERE match_id = ?" - team_rows = pd.read_sql_query(query_teams, conn, params=(mid,)) - - for _, t_row in team_rows.iterrows(): - # Check if sid is in group_uids (which contains UIDs, not SteamIDs!) - # We need to map SteamID -> UID - # Use dim_players or fact_match_players - q_uid = "SELECT uid FROM fact_match_players WHERE match_id = ? AND steam_id_64 = ?" - uid_res = conn.execute(q_uid, (mid, sid)).fetchone() - if not uid_res: - continue - - uid = str(uid_res[0]) - group_uids = str(t_row['group_uids']).split(',') - - if uid in group_uids: - role = t_row['group_fh_role'] - print(f"Match {mid}: Steam {sid} (UID {uid}) is on Side {side} in R1.") - print(f" Found in Group {t_row['group_id']} with FH Role {role}.") - print(f" MAPPING: Role {role} = {side}") - break - - conn.close() - -if __name__ == "__main__": - check_mapping() diff --git a/scripts/check_tables.py b/scripts/check_tables.py deleted file mode 100644 index d8df0ad..0000000 --- a/scripts/check_tables.py +++ /dev/null @@ -1,43 +0,0 @@ -import sqlite3 -import os - -DB_PATH = r'd:\Documents\trae_projects\yrtv\database\L2\L2_Main.sqlite' - -def check_tables(): - if not os.path.exists(DB_PATH): - print(f"DB not found: {DB_PATH}") - return - - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - tables = [ - 'dim_players', 'dim_maps', - 'fact_matches', 'fact_match_teams', - 'fact_match_players', 'fact_match_players_ct', 'fact_match_players_t', - 'fact_rounds', 'fact_round_events', 'fact_round_player_economy' - ] - - print(f"--- L2 Database Check: {DB_PATH} ---") - for table in tables: - try: - cursor.execute(f"SELECT COUNT(*) FROM {table}") - count = cursor.fetchone()[0] - print(f"{table:<25}: {count:>6} rows") - - # Simple column check for recently added columns - if table == 'fact_match_players': - cursor.execute(f"PRAGMA table_info({table})") - cols = [info[1] for info in cursor.fetchall()] - if 'util_flash_usage' in cols: - print(f" [OK] util_flash_usage exists") - else: - print(f" [ERR] util_flash_usage MISSING") - - except Exception as e: - print(f"{table:<25}: [ERROR] {e}") - - conn.close() - -if __name__ == "__main__": - check_tables() diff --git a/scripts/debug_db.py b/scripts/debug_db.py deleted file mode 100644 index a755fd1..0000000 --- a/scripts/debug_db.py +++ /dev/null @@ -1,63 +0,0 @@ -import sqlite3 -import pandas as pd -import os - -L2_PATH = r'd:\Documents\trae_projects\yrtv\database\L2\L2_Main.sqlite' -WEB_PATH = r'd:\Documents\trae_projects\yrtv\database\Web\Web_App.sqlite' - -def debug_db(): - # --- L2 Checks --- - conn = sqlite3.connect(L2_PATH) - - print("--- Data Source Type Distribution ---") - try: - df = pd.read_sql_query("SELECT data_source_type, COUNT(*) as cnt FROM fact_matches GROUP BY data_source_type", conn) - print(df) - except Exception as e: - print(f"Error: {e}") - - print("\n--- Economy Table Count ---") - try: - count = conn.execute("SELECT COUNT(*) FROM fact_round_player_economy").fetchone()[0] - print(f"Rows: {count}") - except Exception as e: - print(f"Error: {e}") - - print("\n--- Check util_flash_usage in fact_match_players ---") - try: - cursor = conn.cursor() - cursor.execute("PRAGMA table_info(fact_match_players)") - cols = [info[1] for info in cursor.fetchall()] - if 'util_flash_usage' in cols: - print("Column 'util_flash_usage' EXISTS.") - nz = conn.execute("SELECT COUNT(*) FROM fact_match_players WHERE util_flash_usage > 0").fetchone()[0] - print(f"Rows with util_flash_usage > 0: {nz}") - else: - print("Column 'util_flash_usage' MISSING.") - except Exception as e: - print(f"Error: {e}") - - conn.close() - - # --- Web DB Checks --- - print("\n--- Web DB Check ---") - if not os.path.exists(WEB_PATH): - print(f"Web DB not found at {WEB_PATH}") - return - - try: - conn_web = sqlite3.connect(WEB_PATH) - cursor = conn_web.cursor() - cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") - tables = cursor.fetchall() - print(f"Tables: {[t[0] for t in tables]}") - - if 'player_metadata' in [t[0] for t in tables]: - count = conn_web.execute("SELECT COUNT(*) FROM player_metadata").fetchone()[0] - print(f"player_metadata rows: {count}") - conn_web.close() - except Exception as e: - print(f"Error checking Web DB: {e}") - -if __name__ == "__main__": - debug_db() diff --git a/scripts/debug_integrity.py b/scripts/debug_integrity.py deleted file mode 100644 index a8e0ac6..0000000 --- a/scripts/debug_integrity.py +++ /dev/null @@ -1,34 +0,0 @@ -import sqlite3 -import os - -BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -L2_PATH = os.path.join(BASE_DIR, 'database', 'L2', 'L2_Main.sqlite') - -def check_db_integrity(): - print(f"Checking DB at: {L2_PATH}") - if not os.path.exists(L2_PATH): - print("CRITICAL: Database file does not exist!") - return - - try: - conn = sqlite3.connect(L2_PATH) - cursor = conn.cursor() - - # Check integrity - print("Running PRAGMA integrity_check...") - cursor.execute("PRAGMA integrity_check") - print(f"Integrity: {cursor.fetchone()}") - - # Check specific user again - cursor.execute("SELECT steam_id_64, username FROM dim_players WHERE username LIKE '%jacky%'") - rows = cursor.fetchall() - print(f"Direct DB check found {len(rows)} rows matching '%jacky%':") - for r in rows: - print(r) - - conn.close() - except Exception as e: - print(f"DB Error: {e}") - -if __name__ == '__main__': - check_db_integrity() diff --git a/scripts/debug_jacky.py b/scripts/debug_jacky.py deleted file mode 100644 index 2b20453..0000000 --- a/scripts/debug_jacky.py +++ /dev/null @@ -1,39 +0,0 @@ -import sqlite3 -import os - -BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -L2_PATH = os.path.join(BASE_DIR, 'database', 'L2', 'L2_Main.sqlite') - -def check_jacky(): - print(f"Checking L2 database at: {L2_PATH}") - conn = sqlite3.connect(L2_PATH) - cursor = conn.cursor() - - search_term = 'jacky' - print(f"\nSearching for '%{search_term}%' (Case Insensitive test):") - - # Standard LIKE - cursor.execute("SELECT steam_id_64, username FROM dim_players WHERE username LIKE ?", (f'%{search_term}%',)) - results = cursor.fetchall() - print(f"LIKE results: {len(results)}") - for r in results: - print(r) - - # Case insensitive explicit - print("\nSearching with LOWER():") - cursor.execute("SELECT steam_id_64, username FROM dim_players WHERE LOWER(username) LIKE LOWER(?)", (f'%{search_term}%',)) - results_lower = cursor.fetchall() - print(f"LOWER() results: {len(results_lower)}") - for r in results_lower: - print(r) - - # Check jacky0987 specifically - print("\nChecking specific username 'jacky0987':") - cursor.execute("SELECT steam_id_64, username FROM dim_players WHERE username = 'jacky0987'") - specific = cursor.fetchone() - print(f"Specific match: {specific}") - - conn.close() - -if __name__ == '__main__': - check_jacky() diff --git a/scripts/init_web_db.py b/scripts/init_web_db.py deleted file mode 100644 index 2566d00..0000000 --- a/scripts/init_web_db.py +++ /dev/null @@ -1,84 +0,0 @@ -import sqlite3 -import os - -# Define database path -BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -DB_PATH = os.path.join(BASE_DIR, 'database', 'Web', 'Web_App.sqlite') - -def init_db(): - print(f"Initializing Web database at: {DB_PATH}") - - # Create directory if not exists - os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Create Tables - tables = [ - """ - CREATE TABLE IF NOT EXISTS team_lineups ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - description TEXT, - player_ids_json TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - """, - """ - CREATE TABLE IF NOT EXISTS player_metadata ( - steam_id_64 TEXT PRIMARY KEY, - notes TEXT, - tags TEXT, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - """, - """ - CREATE TABLE IF NOT EXISTS strategy_boards ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT, - map_name TEXT, - data_json TEXT, - created_by TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - """, - """ - CREATE TABLE IF NOT EXISTS wiki_pages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - path TEXT UNIQUE, - title TEXT, - content TEXT, - updated_by TEXT, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - """, - """ - CREATE TABLE IF NOT EXISTS comments ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id TEXT, - username TEXT, - target_type TEXT, - target_id TEXT, - content TEXT, - likes INTEGER DEFAULT 0, - is_hidden INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - """ - ] - - for sql in tables: - try: - cursor.execute(sql) - print("Executed SQL successfully.") - except Exception as e: - print(f"Error executing SQL: {e}") - - conn.commit() - conn.close() - print("Web database initialized successfully.") - -if __name__ == '__main__': - init_db() diff --git a/scripts/run_rebuild.py b/scripts/run_rebuild.py deleted file mode 100644 index 4158415..0000000 --- a/scripts/run_rebuild.py +++ /dev/null @@ -1,18 +0,0 @@ -import sys -import os - -# Add project root to path -current_dir = os.path.dirname(os.path.abspath(__file__)) -project_root = os.path.dirname(current_dir) -sys.path.append(project_root) - -from web.services.feature_service import FeatureService - -print("Starting Rebuild...") -try: - count = FeatureService.rebuild_all_features(min_matches=1) - print(f"Rebuild Complete. Processed {count} players.") -except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() diff --git a/scripts/run_rebuild_fix.py b/scripts/run_rebuild_fix.py deleted file mode 100644 index cea5f04..0000000 --- a/scripts/run_rebuild_fix.py +++ /dev/null @@ -1,14 +0,0 @@ -from web.app import create_app -from web.services.feature_service import FeatureService -import sys -import os - -# Ensure project root is in path -sys.path.append(os.getcwd()) - -app = create_app() - -with app.app_context(): - print("Starting Feature Rebuild...") - count = FeatureService.rebuild_all_features() - print(f"Rebuild Complete. Processed {count} players.") diff --git a/scripts/update_l2_schema_utility.py b/scripts/update_l2_schema_utility.py deleted file mode 100644 index 95b69e8..0000000 --- a/scripts/update_l2_schema_utility.py +++ /dev/null @@ -1,30 +0,0 @@ -import sys -import os -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -import sqlite3 -from web.config import Config - -conn = sqlite3.connect(Config.DB_L2_PATH) -cursor = conn.cursor() - -columns = [ - 'util_flash_usage', - 'util_smoke_usage', - 'util_molotov_usage', - 'util_he_usage', - 'util_decoy_usage' -] - -for col in columns: - try: - cursor.execute(f"ALTER TABLE fact_match_players ADD COLUMN {col} INTEGER DEFAULT 0") - print(f"Added column {col}") - except sqlite3.OperationalError as e: - if "duplicate column name" in str(e): - print(f"Column {col} already exists.") - else: - print(f"Error adding {col}: {e}") - -conn.commit() -conn.close() diff --git a/scripts/update_l3_schema.py b/scripts/update_l3_schema.py deleted file mode 100644 index a4b3db6..0000000 --- a/scripts/update_l3_schema.py +++ /dev/null @@ -1,39 +0,0 @@ -import sqlite3 -import os - -DB_PATH = r'd:\Documents\trae_projects\yrtv\database\L3\L3_Features.sqlite' - -def add_columns(): - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Check existing columns - cursor.execute("PRAGMA table_info(dm_player_features)") - columns = [row[1] for row in cursor.fetchall()] - - new_columns = [ - 'score_bat', 'score_sta', 'score_hps', 'score_ptl', 'score_tct', 'score_util', - 'bat_avg_duel_win_rate', 'bat_kd_diff_high_elo', 'bat_win_rate_close', - 'sta_time_rating_corr', 'sta_fatigue_decay', - 'hps_match_point_win_rate', 'hps_comeback_kd_diff', 'hps_pressure_entry_rate', - 'ptl_pistol_win_rate', 'ptl_pistol_kd', - 'util_avg_flash_enemy' - ] - - for col in new_columns: - if col not in columns: - print(f"Adding column: {col}") - try: - cursor.execute(f"ALTER TABLE dm_player_features ADD COLUMN {col} REAL") - except Exception as e: - print(f"Error adding {col}: {e}") - - conn.commit() - conn.close() - print("Schema update complete.") - -if __name__ == "__main__": - if not os.path.exists(DB_PATH): - print("L3 DB not found, skipping schema update (will be created by build script).") - else: - add_columns() diff --git a/scripts/update_l3_schema_full.py b/scripts/update_l3_schema_full.py deleted file mode 100644 index 141045f..0000000 --- a/scripts/update_l3_schema_full.py +++ /dev/null @@ -1,82 +0,0 @@ -import sqlite3 -import os - -DB_PATH = r'd:\Documents\trae_projects\yrtv\database\L3\L3_Features.sqlite' - -def update_schema(): - if not os.path.exists(DB_PATH): - print("L3 DB not found.") - return - - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Get existing columns - cursor.execute("PRAGMA table_info(dm_player_features)") - existing_cols = {row[1] for row in cursor.fetchall()} - - # List of columns to ensure exist - # Copied from schema.sql - required_columns = [ - # Basic - 'basic_avg_rating', 'basic_avg_kd', 'basic_avg_adr', 'basic_avg_kast', 'basic_avg_rws', - 'basic_avg_headshot_kills', 'basic_headshot_rate', - 'basic_avg_first_kill', 'basic_avg_first_death', 'basic_first_kill_rate', 'basic_first_death_rate', - 'basic_avg_kill_2', 'basic_avg_kill_3', 'basic_avg_kill_4', 'basic_avg_kill_5', - 'basic_avg_assisted_kill', 'basic_avg_perfect_kill', 'basic_avg_revenge_kill', - 'basic_avg_awp_kill', 'basic_avg_jump_count', - 'basic_avg_mvps', 'basic_avg_plants', 'basic_avg_defuses', 'basic_avg_flash_assists', - - # STA - 'sta_last_30_rating', 'sta_win_rating', 'sta_loss_rating', 'sta_rating_volatility', - 'sta_time_rating_corr', 'sta_fatigue_decay', - - # BAT - 'bat_kd_diff_high_elo', 'bat_kd_diff_low_elo', 'bat_avg_duel_win_rate', 'bat_avg_duel_freq', - 'bat_win_rate_close', 'bat_win_rate_mid', 'bat_win_rate_far', - - # HPS - 'hps_clutch_win_rate_1v1', 'hps_clutch_win_rate_1v2', 'hps_clutch_win_rate_1v3_plus', - 'hps_match_point_win_rate', 'hps_undermanned_survival_time', 'hps_pressure_entry_rate', - 'hps_momentum_multikill_rate', 'hps_tilt_rating_drop', 'hps_clutch_rating_rise', - 'hps_comeback_kd_diff', 'hps_losing_streak_kd_diff', - - # PTL - 'ptl_pistol_kills', 'ptl_pistol_multikills', 'ptl_pistol_win_rate', 'ptl_pistol_kd', 'ptl_pistol_util_efficiency', - - # SIDE - 'side_rating_ct', 'side_rating_t', 'side_kd_ct', 'side_kd_t', - 'side_win_rate_ct', 'side_win_rate_t', - 'side_first_kill_rate_ct', 'side_first_kill_rate_t', - 'side_kd_diff_ct_t', - 'side_kast_ct', 'side_kast_t', - 'side_rws_ct', 'side_rws_t', - 'side_first_death_rate_ct', 'side_first_death_rate_t', - 'side_multikill_rate_ct', 'side_multikill_rate_t', - 'side_headshot_rate_ct', 'side_headshot_rate_t', - 'side_defuses_ct', 'side_plants_t', - 'side_obj_ct', 'side_obj_t', - 'side_planted_bomb_count', 'side_defused_bomb_count', - - # UTIL - 'util_avg_nade_dmg', 'util_avg_flash_time', 'util_avg_flash_enemy', 'util_avg_flash_team', 'util_usage_rate', - - # Scores - 'score_bat', 'score_sta', 'score_hps', 'score_ptl', 'score_tct', 'score_util' - ] - - for col in required_columns: - if col not in existing_cols: - print(f"Adding missing column: {col}") - try: - # Most are REAL, integers are fine as REAL in sqlite usually, or use affinity - cursor.execute(f"ALTER TABLE dm_player_features ADD COLUMN {col} REAL") - except Exception as e: - print(f"Failed to add {col}: {e}") - - conn.commit() - conn.close() - print("Schema update check complete.") - -if __name__ == "__main__": - update_schema() diff --git a/web/routes/players.py b/web/routes/players.py index 0ab9287..3fb7344 100644 --- a/web/routes/players.py +++ b/web/routes/players.py @@ -98,6 +98,49 @@ def detail(steam_id): return "Player not found", 404 features = FeatureService.get_player_features(steam_id) + + # --- New: Fetch Detailed Stats from L2 (Clutch, Multi-Kill, Multi-Assist) --- + sql_l2 = """ + SELECT + SUM(clutch_1v1) as c1, SUM(clutch_1v2) as c2, SUM(clutch_1v3) as c3, SUM(clutch_1v4) as c4, SUM(clutch_1v5) as c5, + SUM(kill_2) as k2, SUM(kill_3) as k3, SUM(kill_4) as k4, SUM(kill_5) as k5, + SUM(many_assists_cnt2) as a2, SUM(many_assists_cnt3) as a3, SUM(many_assists_cnt4) as a4, SUM(many_assists_cnt5) as a5, + COUNT(*) as matches, + SUM(round_total) as total_rounds + FROM fact_match_players + WHERE steam_id_64 = ? + """ + l2_stats = query_db('l2', sql_l2, [steam_id], one=True) + l2_stats = dict(l2_stats) if l2_stats else {} + + # Fetch T/CT splits for comparison + # Note: We use SUM(clutch...) as Total Clutch Wins. We don't have attempts, so 'Win Rate' is effectively Wins/Rounds or just Wins count. + # User asked for 'Win Rate', but without attempts data, we'll provide Rate per Round or just Count. + # Let's provide Rate per Round for Multi-Kill/Assist, and maybe just Count for Clutch? + # User said: "总残局胜率...分t和ct在下方加入对比". + # Since we found clutch == end in DB, we treat it as Wins. We can't calc Win %. + # We will display "Clutch Wins / Round" or just "Clutch Wins". + + sql_side = """ + SELECT + 'T' as side, + SUM(clutch_1v1+clutch_1v2+clutch_1v3+clutch_1v4+clutch_1v5) as total_clutch, + SUM(kill_2+kill_3+kill_4+kill_5) as total_multikill, + SUM(many_assists_cnt2+many_assists_cnt3+many_assists_cnt4+many_assists_cnt5) as total_multiassist, + SUM(round_total) as rounds + FROM fact_match_players_t WHERE steam_id_64 = ? + UNION ALL + SELECT + 'CT' as side, + SUM(clutch_1v1+clutch_1v2+clutch_1v3+clutch_1v4+clutch_1v5) as total_clutch, + SUM(kill_2+kill_3+kill_4+kill_5) as total_multikill, + SUM(many_assists_cnt2+many_assists_cnt3+many_assists_cnt4+many_assists_cnt5) as total_multiassist, + SUM(round_total) as rounds + FROM fact_match_players_ct WHERE steam_id_64 = ? + """ + side_rows = query_db('l2', sql_side, [steam_id, steam_id]) + side_stats = {row['side']: dict(row) for row in side_rows} if side_rows else {} + # Ensure basic stats fallback if features missing or incomplete basic = StatsService.get_player_basic_stats(steam_id) @@ -157,7 +200,16 @@ def detail(steam_id): }) map_stats_list.sort(key=lambda x: x['matches'], reverse=True) - return render_template('players/profile.html', player=player, features=features, comments=comments, metadata=metadata, history=history, distribution=distribution, map_stats=map_stats_list) + return render_template('players/profile.html', + player=player, + features=features, + comments=comments, + metadata=metadata, + history=history, + distribution=distribution, + map_stats=map_stats_list, + l2_stats=l2_stats, + side_stats=side_stats) @bp.route('/comment//like', methods=['POST']) def like_comment(comment_id): diff --git a/web/services/feature_service.py b/web/services/feature_service.py index 04a765f..b1cf451 100644 --- a/web/services/feature_service.py +++ b/web/services/feature_service.py @@ -295,9 +295,7 @@ class FeatureService: SUM(first_death) as sum_fd, SUM(clutch_1v1) as sum_1v1, SUM(clutch_1v2) as sum_1v2, - SUM(clutch_1v3) as sum_1v3, - SUM(clutch_1v4) as sum_1v4, - SUM(clutch_1v5) as sum_1v5, + SUM(clutch_1v3) + SUM(clutch_1v4) + SUM(clutch_1v5) as sum_1v3p, SUM(kill_2) as sum_2k, SUM(kill_3) as sum_3k, SUM(kill_4) as sum_4k, @@ -342,15 +340,6 @@ class FeatureService: df['basic_avg_kill_3'] = df['sum_3k'] / df['matches_played'] df['basic_avg_kill_4'] = df['sum_4k'] / df['matches_played'] df['basic_avg_kill_5'] = df['sum_5k'] / df['matches_played'] - - # New Metrics - df['basic_multi_kill_rate'] = (df['sum_2k'] + df['sum_3k'] + df['sum_4k'] + df['sum_5k']) / df['rounds_played'].replace(0, 1) - df['basic_total_1v1'] = df['sum_1v1'] - df['basic_total_1v2'] = df['sum_1v2'] - df['basic_total_1v3'] = df['sum_1v3'] - df['basic_total_1v4'] = df['sum_1v4'] - df['basic_total_1v5'] = df['sum_1v5'] - df['basic_avg_assisted_kill'] = df['sum_assist'] / df['matches_played'] df['basic_avg_perfect_kill'] = df['sum_perfect'] / df['matches_played'] df['basic_avg_revenge_kill'] = df['sum_revenge'] / df['matches_played'] diff --git a/web/templates/players/profile.html b/web/templates/players/profile.html index c021968..d3f47e7 100644 --- a/web/templates/players/profile.html +++ b/web/templates/players/profile.html @@ -147,11 +147,11 @@ 📊 详细数据面板 (Detailed Stats)
- {% macro detail_item(label, value, key, format_str='{:.2f}', sublabel=None) %} + {% macro detail_item(label, value, key, format_str='{:.2f}', sublabel=None, count_label=None) %} {% set dist = distribution[key] if distribution else None %} -
+
- {{ label }} + {{ label }} {% if dist %} + {{ count_label }} +
+ {% endif %}
{% endmacro %} @@ -222,16 +229,8 @@ {{ detail_item('3K Rounds (三杀)', features['basic_avg_kill_3'], 'basic_avg_kill_3') }} {{ detail_item('4K Rounds (四杀)', features['basic_avg_kill_4'], 'basic_avg_kill_4') }} {{ detail_item('5K Rounds (五杀)', features['basic_avg_kill_5'], 'basic_avg_kill_5') }} - {{ detail_item('Multi-Kill % (多杀率)', features['basic_multi_kill_rate'], 'basic_multi_kill_rate', '{:.1%}') }} - - {{ detail_item('1v1 Wins (1v1胜)', features['basic_total_1v1'], 'basic_total_1v1', '{:.0f}') }} - {{ detail_item('1v2 Wins (1v2胜)', features['basic_total_1v2'], 'basic_total_1v2', '{:.0f}') }} - {{ detail_item('1v3 Wins (1v3胜)', features['basic_total_1v3'], 'basic_total_1v3', '{:.0f}') }} - {{ detail_item('1v4 Wins (1v4胜)', features['basic_total_1v4'], 'basic_total_1v4', '{:.0f}') }} - {{ detail_item('1v5 Wins (1v5胜)', features['basic_total_1v5'], 'basic_total_1v5', '{:.0f}') }} - - + {{ detail_item('Perfect Kills (无伤杀)', features['basic_avg_perfect_kill'], 'basic_avg_perfect_kill') }} {{ detail_item('Revenge Kills (复仇杀)', features['basic_avg_revenge_kill'], 'basic_avg_revenge_kill') }}
@@ -296,15 +295,34 @@
+ +
+

+ SPECIAL (Clutch & Multi) +

+ {% set rounds = l2_stats.get('total_rounds', 0) or 1 %} +
+ {{ detail_item('1v1 Win%', (l2_stats.get('c1', 0) or 0) / rounds, 'l2_c1', '{:.1%}', count_label=l2_stats.get('c1', 0)) }} + {{ detail_item('1v2 Win%', (l2_stats.get('c2', 0) or 0) / rounds, 'l2_c2', '{:.1%}', count_label=l2_stats.get('c2', 0)) }} + {{ detail_item('1v3 Win%', (l2_stats.get('c3', 0) or 0) / rounds, 'l2_c3', '{:.1%}', count_label=l2_stats.get('c3', 0)) }} + {{ detail_item('1v4 Win%', (l2_stats.get('c4', 0) or 0) / rounds, 'l2_c4', '{:.1%}', count_label=l2_stats.get('c4', 0)) }} + {{ detail_item('1v5 Win%', (l2_stats.get('c5', 0) or 0) / rounds, 'l2_c5', '{:.1%}', count_label=l2_stats.get('c5', 0)) }} + + {% set mk_count = (l2_stats.get('k2', 0) or 0) + (l2_stats.get('k3', 0) or 0) + (l2_stats.get('k4', 0) or 0) + (l2_stats.get('k5', 0) or 0) %} + {% set ma_count = (l2_stats.get('a2', 0) or 0) + (l2_stats.get('a3', 0) or 0) + (l2_stats.get('a4', 0) or 0) + (l2_stats.get('a5', 0) or 0) %} + + {{ detail_item('Multi-Kill Rate', mk_count / rounds, 'l2_mk', '{:.1%}', count_label=mk_count) }} + {{ detail_item('Multi-Assist Rate', ma_count / rounds, 'l2_ma', '{:.1%}', count_label=ma_count) }} +
+
+

SIDE (T/CT Preference)

- {% macro vs_item(label, t_key, ct_key, format_str='{:.2f}') %} - {% set t_val = features[t_key] or 0 %} - {% set ct_val = features[ct_key] or 0 %} + {% macro vs_item_val(label, t_val, ct_val, format_str='{:.2f}') %} {% set diff = ct_val - t_val %} {# Dynamic Sizing #} @@ -367,6 +385,10 @@
{% endmacro %} + {% macro vs_item(label, t_key, ct_key, format_str='{:.2f}') %} + {{ vs_item_val(label, features[t_key] or 0, features[ct_key] or 0, format_str) }} + {% endmacro %} +
{{ vs_item('Rating (Rating/KD)', 'side_rating_t', 'side_rating_ct') }} {{ vs_item('KD Ratio', 'side_kd_t', 'side_kd_ct') }} @@ -375,8 +397,23 @@ {{ vs_item('First Death Rate (首死率)', 'side_first_death_rate_t', 'side_first_death_rate_ct', '{:.1%}') }} {{ vs_item('KAST (贡献率)', 'side_kast_t', 'side_kast_ct', '{:.1%}') }} {{ vs_item('RWS (Round Win Share)', 'side_rws_t', 'side_rws_ct') }} - {{ vs_item('Multi-Kill Rate (多杀率)', 'side_multikill_rate_t', 'side_multikill_rate_ct', '{:.1%}') }} {{ vs_item('Headshot Rate (爆头率)', 'side_headshot_rate_t', 'side_headshot_rate_ct', '{:.1%}') }} + + {# New Comparisons #} + {% set t_rounds = side_stats.get('T', {}).get('rounds', 0) or 1 %} + {% set ct_rounds = side_stats.get('CT', {}).get('rounds', 0) or 1 %} + + {% set t_clutch = (side_stats.get('T', {}).get('total_clutch', 0) or 0) / t_rounds %} + {% set ct_clutch = (side_stats.get('CT', {}).get('total_clutch', 0) or 0) / ct_rounds %} + {{ vs_item_val('Clutch Win Rate (残局率)', t_clutch, ct_clutch, '{:.1%}') }} + + {% set t_mk = (side_stats.get('T', {}).get('total_multikill', 0) or 0) / t_rounds %} + {% set ct_mk = (side_stats.get('CT', {}).get('total_multikill', 0) or 0) / ct_rounds %} + {{ vs_item_val('Multi-Kill Rate (多杀率)', t_mk, ct_mk, '{:.1%}') }} + + {% set t_ma = (side_stats.get('T', {}).get('total_multiassist', 0) or 0) / t_rounds %} + {% set ct_ma = (side_stats.get('CT', {}).get('total_multiassist', 0) or 0) / ct_rounds %} + {{ vs_item_val('Multi-Assist Rate (多助攻)', t_ma, ct_ma, '{:.1%}') }}