Compare commits
3 Commits
a148c2d511
...
50428ae2ac
| Author | SHA1 | Date | |
|---|---|---|---|
| 50428ae2ac | |||
| 0be68a86f6 | |||
| 28dc02c0c4 |
@@ -1342,5 +1342,94 @@ def save_match(cursor, m: MatchData):
|
||||
m.match_id, r.round_num, pe.steam_id_64, pe.side, pe.start_money, pe.equipment_value, pe.main_weapon, pe.has_helmet, pe.has_defuser, pe.round_performance_score
|
||||
))
|
||||
|
||||
# 6. Calculate & Save Clutch Attempts
|
||||
_calculate_and_save_clutch_attempts(cursor, m.match_id, m.round_list_raw)
|
||||
|
||||
def _calculate_and_save_clutch_attempts(cursor, match_id, round_list_raw):
|
||||
if not round_list_raw:
|
||||
return
|
||||
|
||||
try:
|
||||
round_list = json.loads(round_list_raw)
|
||||
except:
|
||||
return
|
||||
|
||||
player_attempts = {}
|
||||
|
||||
for round_data in round_list:
|
||||
all_kills = round_data.get('all_kill', [])
|
||||
if not all_kills:
|
||||
continue
|
||||
|
||||
team_members = {1: set(), 2: set()}
|
||||
|
||||
# Scan for team members
|
||||
for k in all_kills:
|
||||
if k.get('attacker') and k['attacker'].get('steamid_64'):
|
||||
tid = k['attacker'].get('team')
|
||||
if tid in [1, 2]:
|
||||
team_members[tid].add(k['attacker']['steamid_64'])
|
||||
if k.get('victim') and k['victim'].get('steamid_64'):
|
||||
tid = k['victim'].get('team')
|
||||
if tid in [1, 2]:
|
||||
team_members[tid].add(k['victim']['steamid_64'])
|
||||
|
||||
if not team_members[1] or not team_members[2]:
|
||||
continue
|
||||
|
||||
alive = {1: team_members[1].copy(), 2: team_members[2].copy()}
|
||||
clutch_triggered_players = set()
|
||||
|
||||
# Sort kills by time
|
||||
sorted_kills = sorted(all_kills, key=lambda x: x.get('pasttime', 0))
|
||||
|
||||
for k in sorted_kills:
|
||||
victim = k.get('victim')
|
||||
if not victim: continue
|
||||
|
||||
v_sid = victim.get('steamid_64')
|
||||
v_team = victim.get('team')
|
||||
|
||||
if v_team not in [1, 2] or v_sid not in alive[v_team]:
|
||||
continue
|
||||
|
||||
alive[v_team].remove(v_sid)
|
||||
|
||||
if len(alive[v_team]) == 1:
|
||||
survivor_sid = list(alive[v_team])[0]
|
||||
if survivor_sid not in clutch_triggered_players:
|
||||
opponent_team = 3 - v_team
|
||||
opponents_alive_count = len(alive[opponent_team])
|
||||
|
||||
if opponents_alive_count >= 1:
|
||||
if survivor_sid not in player_attempts:
|
||||
player_attempts[survivor_sid] = {'1v1': 0, '1v2': 0, '1v3': 0, '1v4': 0, '1v5': 0}
|
||||
|
||||
n = min(opponents_alive_count, 5)
|
||||
key = f'1v{n}'
|
||||
player_attempts[survivor_sid][key] += 1
|
||||
clutch_triggered_players.add(survivor_sid)
|
||||
|
||||
# Save to DB
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS fact_match_clutch_attempts (
|
||||
match_id TEXT,
|
||||
steam_id_64 TEXT,
|
||||
attempt_1v1 INTEGER DEFAULT 0,
|
||||
attempt_1v2 INTEGER DEFAULT 0,
|
||||
attempt_1v3 INTEGER DEFAULT 0,
|
||||
attempt_1v4 INTEGER DEFAULT 0,
|
||||
attempt_1v5 INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (match_id, steam_id_64)
|
||||
)
|
||||
""")
|
||||
|
||||
for pid, att in player_attempts.items():
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO fact_match_clutch_attempts
|
||||
(match_id, steam_id_64, attempt_1v1, attempt_1v2, attempt_1v3, attempt_1v4, attempt_1v5)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (match_id, pid, att['1v1'], att['1v2'], att['1v3'], att['1v4'], att['1v5']))
|
||||
|
||||
if __name__ == "__main__":
|
||||
process_matches()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -98,6 +98,52 @@ CREATE TABLE IF NOT EXISTS dm_player_features (
|
||||
side_kd_diff_ct_t REAL, -- CT KD - T KD
|
||||
|
||||
-- New Side Comparisons
|
||||
side_rating_diff_ct_t REAL,
|
||||
|
||||
-- ==========================================
|
||||
-- 6. Party Size Performance
|
||||
-- ==========================================
|
||||
party_1_win_rate REAL,
|
||||
party_1_rating REAL,
|
||||
party_1_adr REAL,
|
||||
|
||||
party_2_win_rate REAL,
|
||||
party_2_rating REAL,
|
||||
party_2_adr REAL,
|
||||
|
||||
party_3_win_rate REAL,
|
||||
party_3_rating REAL,
|
||||
party_3_adr REAL,
|
||||
|
||||
party_4_win_rate REAL,
|
||||
party_4_rating REAL,
|
||||
party_4_adr REAL,
|
||||
|
||||
party_5_win_rate REAL,
|
||||
party_5_rating REAL,
|
||||
party_5_adr REAL,
|
||||
|
||||
-- ==========================================
|
||||
-- 7. Rating Distribution (Performance Tiers)
|
||||
-- ==========================================
|
||||
rating_dist_carry_rate REAL, -- > 1.5
|
||||
rating_dist_normal_rate REAL, -- 1.0 - 1.5
|
||||
rating_dist_sacrifice_rate REAL, -- 0.6 - 1.0
|
||||
rating_dist_sleeping_rate REAL, -- < 0.6
|
||||
|
||||
-- ==========================================
|
||||
-- 8. ELO Stratification (Performance vs ELO)
|
||||
-- ==========================================
|
||||
elo_lt1200_rating REAL,
|
||||
elo_1200_1400_rating REAL,
|
||||
elo_1400_1600_rating REAL,
|
||||
elo_1600_1800_rating REAL,
|
||||
elo_1800_2000_rating REAL,
|
||||
elo_gt2000_rating REAL,
|
||||
|
||||
-- ==========================================
|
||||
-- 9. More Side Stats (Restored)
|
||||
-- ==========================================
|
||||
side_kast_ct REAL,
|
||||
side_kast_t REAL,
|
||||
side_rws_ct REAL,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
用于测试脚本目录。
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -98,6 +98,51 @@ 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(p.clutch_1v1) as c1, SUM(p.clutch_1v2) as c2, SUM(p.clutch_1v3) as c3, SUM(p.clutch_1v4) as c4, SUM(p.clutch_1v5) as c5,
|
||||
SUM(a.attempt_1v1) as att1, SUM(a.attempt_1v2) as att2, SUM(a.attempt_1v3) as att3, SUM(a.attempt_1v4) as att4, SUM(a.attempt_1v5) as att5,
|
||||
SUM(p.kill_2) as k2, SUM(p.kill_3) as k3, SUM(p.kill_4) as k4, SUM(p.kill_5) as k5,
|
||||
SUM(p.many_assists_cnt2) as a2, SUM(p.many_assists_cnt3) as a3, SUM(p.many_assists_cnt4) as a4, SUM(p.many_assists_cnt5) as a5,
|
||||
COUNT(*) as matches,
|
||||
SUM(p.round_total) as total_rounds
|
||||
FROM fact_match_players p
|
||||
LEFT JOIN fact_match_clutch_attempts a ON p.match_id = a.match_id AND p.steam_id_64 = a.steam_id_64
|
||||
WHERE p.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 +202,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/<int:comment_id>/like', methods=['POST'])
|
||||
def like_comment(comment_id):
|
||||
|
||||
@@ -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']
|
||||
@@ -989,6 +978,163 @@ class FeatureService:
|
||||
df['util_usage_rate'] = df['util_usage_rate_backup'].fillna(0)
|
||||
df.drop(columns=['util_usage_rate_backup'], inplace=True)
|
||||
|
||||
# --- 8. New Feature Dimensions (Party, Rating Dist, ELO) ---
|
||||
# Fetch Base Data for Calculation
|
||||
q_new_feats = f"""
|
||||
SELECT mp.steam_id_64, mp.match_id, mp.match_team_id, mp.team_id,
|
||||
mp.rating, mp.adr, mp.is_win
|
||||
FROM fact_match_players mp
|
||||
WHERE mp.steam_id_64 IN ({placeholders})
|
||||
"""
|
||||
df_base = pd.read_sql_query(q_new_feats, conn, params=valid_ids)
|
||||
|
||||
if not df_base.empty:
|
||||
# 8.1 Party Size Stats
|
||||
# Get party sizes for these matches
|
||||
# We need to query party sizes for ALL matches involved
|
||||
match_ids = df_base['match_id'].unique()
|
||||
if len(match_ids) > 0:
|
||||
match_id_ph = ','.join(['?'] * len(match_ids))
|
||||
q_party_size = f"""
|
||||
SELECT match_id, match_team_id, COUNT(*) as party_size
|
||||
FROM fact_match_players
|
||||
WHERE match_id IN ({match_id_ph}) AND match_team_id > 0
|
||||
GROUP BY match_id, match_team_id
|
||||
"""
|
||||
# Split match_ids into chunks if too many
|
||||
chunk_size = 900
|
||||
party_sizes_list = []
|
||||
for i in range(0, len(match_ids), chunk_size):
|
||||
chunk = match_ids[i:i+chunk_size]
|
||||
chunk_ph = ','.join(['?'] * len(chunk))
|
||||
q_chunk = q_party_size.replace(match_id_ph, chunk_ph)
|
||||
party_sizes_list.append(pd.read_sql_query(q_chunk, conn, params=list(chunk)))
|
||||
|
||||
if party_sizes_list:
|
||||
df_party_sizes = pd.concat(party_sizes_list)
|
||||
|
||||
# Merge party size to base data
|
||||
df_base_party = df_base.merge(df_party_sizes, on=['match_id', 'match_team_id'], how='left')
|
||||
|
||||
# Calculate Stats per Party Size (1-5)
|
||||
# We want columns like party_1_win_rate, party_1_rating, party_1_adr
|
||||
party_stats = df_base_party.groupby(['steam_id_64', 'party_size']).agg({
|
||||
'is_win': 'mean',
|
||||
'rating': 'mean',
|
||||
'adr': 'mean'
|
||||
}).reset_index()
|
||||
|
||||
# Pivot
|
||||
pivoted_party = party_stats.pivot(index='steam_id_64', columns='party_size').reset_index()
|
||||
|
||||
# Flatten and rename
|
||||
new_party_cols = ['steam_id_64']
|
||||
for col in pivoted_party.columns:
|
||||
if col[0] == 'steam_id_64': continue
|
||||
metric, size = col
|
||||
if size in [1, 2, 3, 4, 5]:
|
||||
# metric is is_win, rating, adr
|
||||
metric_name = 'win_rate' if metric == 'is_win' else metric
|
||||
new_party_cols.append(f"party_{int(size)}_{metric_name}")
|
||||
|
||||
# Handle MultiIndex column flattening properly
|
||||
# The pivot creates MultiIndex. We need to construct a flat DataFrame.
|
||||
flat_data = {'steam_id_64': pivoted_party['steam_id_64']}
|
||||
for size in [1, 2, 3, 4, 5]:
|
||||
if size in pivoted_party['is_win'].columns:
|
||||
flat_data[f"party_{size}_win_rate"] = pivoted_party['is_win'][size]
|
||||
if size in pivoted_party['rating'].columns:
|
||||
flat_data[f"party_{size}_rating"] = pivoted_party['rating'][size]
|
||||
if size in pivoted_party['adr'].columns:
|
||||
flat_data[f"party_{size}_adr"] = pivoted_party['adr'][size]
|
||||
|
||||
df_party_flat = pd.DataFrame(flat_data)
|
||||
df = df.merge(df_party_flat, on='steam_id_64', how='left')
|
||||
|
||||
# 8.2 Rating Distribution
|
||||
# rating_dist_carry_rate (>1.5), normal (1.0-1.5), sacrifice (0.6-1.0), sleeping (<0.6)
|
||||
df_base['rating_tier'] = pd.cut(df_base['rating'],
|
||||
bins=[-1, 0.6, 1.0, 1.5, 100],
|
||||
labels=['sleeping', 'sacrifice', 'normal', 'carry'],
|
||||
right=False) # <0.6, 0.6-<1.0, 1.0-<1.5, >=1.5 (wait, cut behavior)
|
||||
# Standard cut: right=True by default (a, b]. We want:
|
||||
# < 0.6
|
||||
# 0.6 <= x < 1.0
|
||||
# 1.0 <= x < 1.5
|
||||
# >= 1.5
|
||||
# So bins=[-inf, 0.6, 1.0, 1.5, inf], right=False -> [a, b)
|
||||
df_base['rating_tier'] = pd.cut(df_base['rating'],
|
||||
bins=[-float('inf'), 0.6, 1.0, 1.5, float('inf')],
|
||||
labels=['sleeping', 'sacrifice', 'normal', 'carry'],
|
||||
right=False)
|
||||
|
||||
# Wait, 1.5 should be Normal or Carry?
|
||||
# User: >1.5 Carry, 1.0~1.5 Normal. So 1.5 is Normal? Or Carry?
|
||||
# Usually inclusive on lower bound.
|
||||
# 1.5 -> Carry (>1.5 usually means >= 1.5 or strictly >).
|
||||
# "1.0~1.5 正常" implies [1.0, 1.5]. ">1.5 Carry" implies (1.5, inf).
|
||||
# Let's assume >= 1.5 is Carry.
|
||||
# So bins: (-inf, 0.6), [0.6, 1.0), [1.0, 1.5), [1.5, inf)
|
||||
# right=False gives [a, b).
|
||||
# So [1.5, inf) is correct for Carry.
|
||||
|
||||
dist_stats = df_base.groupby(['steam_id_64', 'rating_tier']).size().unstack(fill_value=0)
|
||||
# Calculate rates
|
||||
dist_stats = dist_stats.div(dist_stats.sum(axis=1), axis=0)
|
||||
dist_stats.columns = [f"rating_dist_{c}_rate" for c in dist_stats.columns]
|
||||
dist_stats = dist_stats.reset_index()
|
||||
|
||||
df = df.merge(dist_stats, on='steam_id_64', how='left')
|
||||
|
||||
# 8.3 ELO Stratification
|
||||
# Fetch Match Teams ELO
|
||||
if len(match_ids) > 0:
|
||||
q_elo = f"""
|
||||
SELECT match_id, group_id, group_origin_elo
|
||||
FROM fact_match_teams
|
||||
WHERE match_id IN ({match_id_ph})
|
||||
"""
|
||||
# Use chunking again
|
||||
elo_list = []
|
||||
for i in range(0, len(match_ids), chunk_size):
|
||||
chunk = match_ids[i:i+chunk_size]
|
||||
chunk_ph = ','.join(['?'] * len(chunk))
|
||||
q_chunk = q_elo.replace(match_id_ph, chunk_ph)
|
||||
elo_list.append(pd.read_sql_query(q_chunk, conn, params=list(chunk)))
|
||||
|
||||
if elo_list:
|
||||
df_elo_teams = pd.concat(elo_list)
|
||||
|
||||
# Merge to get Opponent ELO
|
||||
# Player has match_id, team_id.
|
||||
# Join on match_id.
|
||||
# Filter where group_id != team_id
|
||||
df_merged_elo = df_base.merge(df_elo_teams, on='match_id', how='left')
|
||||
df_merged_elo = df_merged_elo[df_merged_elo['group_id'] != df_merged_elo['team_id']]
|
||||
|
||||
# Now df_merged_elo has 'group_origin_elo' which is Opponent ELO
|
||||
# Binning: <1200, 1200-1400, 1400-1600, 1600-1800, 1800-2000, >2000
|
||||
# bins: [-inf, 1200, 1400, 1600, 1800, 2000, inf]
|
||||
elo_bins = [-float('inf'), 1200, 1400, 1600, 1800, 2000, float('inf')]
|
||||
elo_labels = ['lt1200', '1200_1400', '1400_1600', '1600_1800', '1800_2000', 'gt2000']
|
||||
|
||||
df_merged_elo['elo_bin'] = pd.cut(df_merged_elo['group_origin_elo'], bins=elo_bins, labels=elo_labels, right=False)
|
||||
|
||||
elo_stats = df_merged_elo.groupby(['steam_id_64', 'elo_bin']).agg({
|
||||
'rating': 'mean'
|
||||
}).unstack(fill_value=0) # We only need rating for now
|
||||
|
||||
# Rename columns
|
||||
# elo_stats columns are MultiIndex (rating, bin).
|
||||
# We want: elo_{bin}_rating
|
||||
flat_elo_data = {'steam_id_64': elo_stats.index}
|
||||
for bin_label in elo_labels:
|
||||
if bin_label in elo_stats['rating'].columns:
|
||||
flat_elo_data[f"elo_{bin_label}_rating"] = elo_stats['rating'][bin_label].values
|
||||
|
||||
df_elo_flat = pd.DataFrame(flat_elo_data)
|
||||
df = df.merge(df_elo_flat, on='steam_id_64', how='left')
|
||||
|
||||
# Final Mappings
|
||||
df['total_matches'] = df['matches_played']
|
||||
|
||||
|
||||
@@ -627,6 +627,52 @@ class StatsService:
|
||||
if target_steam_id not in stats_map:
|
||||
stats_map[target_steam_id] = {}
|
||||
|
||||
# --- New: Enrich with L2 Clutch/Multi Stats for Distribution ---
|
||||
l2_placeholders = ','.join('?' for _ in active_roster_ids)
|
||||
sql_l2 = f"""
|
||||
SELECT
|
||||
p.steam_id_64,
|
||||
SUM(p.clutch_1v1) as c1, SUM(p.clutch_1v2) as c2, SUM(p.clutch_1v3) as c3, SUM(p.clutch_1v4) as c4, SUM(p.clutch_1v5) as c5,
|
||||
SUM(a.attempt_1v1) as att1, SUM(a.attempt_1v2) as att2, SUM(a.attempt_1v3) as att3, SUM(a.attempt_1v4) as att4, SUM(a.attempt_1v5) as att5,
|
||||
SUM(p.kill_2) as k2, SUM(p.kill_3) as k3, SUM(p.kill_4) as k4, SUM(p.kill_5) as k5,
|
||||
SUM(p.many_assists_cnt2) as a2, SUM(p.many_assists_cnt3) as a3, SUM(p.many_assists_cnt4) as a4, SUM(p.many_assists_cnt5) as a5,
|
||||
SUM(p.round_total) as total_rounds
|
||||
FROM fact_match_players p
|
||||
LEFT JOIN fact_match_clutch_attempts a ON p.match_id = a.match_id AND p.steam_id_64 = a.steam_id_64
|
||||
WHERE CAST(p.steam_id_64 AS TEXT) IN ({l2_placeholders})
|
||||
GROUP BY p.steam_id_64
|
||||
"""
|
||||
l2_rows = query_db('l2', sql_l2, active_roster_ids)
|
||||
|
||||
for r in l2_rows:
|
||||
sid = str(r['steam_id_64'])
|
||||
if sid not in stats_map:
|
||||
stats_map[sid] = {}
|
||||
|
||||
# Clutch Rates
|
||||
for i in range(1, 6):
|
||||
c = r[f'c{i}'] or 0
|
||||
att = r[f'att{i}'] or 0
|
||||
rate = (c / att) if att > 0 else 0
|
||||
stats_map[sid][f'clutch_rate_1v{i}'] = rate
|
||||
|
||||
# Multi-Kill Rates
|
||||
rounds = r['total_rounds'] or 1 # Avoid div by 0
|
||||
total_mk = 0
|
||||
for i in range(2, 6):
|
||||
k = r[f'k{i}'] or 0
|
||||
total_mk += k
|
||||
stats_map[sid][f'multikill_rate_{i}k'] = k / rounds
|
||||
stats_map[sid]['total_multikill_rate'] = total_mk / rounds
|
||||
|
||||
# Multi-Assist Rates
|
||||
total_ma = 0
|
||||
for i in range(2, 6):
|
||||
a = r[f'a{i}'] or 0
|
||||
total_ma += a
|
||||
stats_map[sid][f'multiassist_rate_{i}a'] = a / rounds
|
||||
stats_map[sid]['total_multiassist_rate'] = total_ma / rounds
|
||||
|
||||
# 3. Calculate Distribution for ALL metrics
|
||||
# Define metrics list (must match Detailed Panel keys)
|
||||
metrics = [
|
||||
@@ -648,7 +694,22 @@ class StatsService:
|
||||
'side_multikill_rate_ct', 'side_multikill_rate_t',
|
||||
'side_headshot_rate_ct', 'side_headshot_rate_t',
|
||||
'side_defuses_ct', 'side_plants_t',
|
||||
'util_avg_nade_dmg', 'util_avg_flash_time', 'util_avg_flash_enemy', 'util_usage_rate'
|
||||
'util_avg_nade_dmg', 'util_avg_flash_time', 'util_avg_flash_enemy', 'util_usage_rate',
|
||||
# New: Party Size Stats
|
||||
'party_1_win_rate', 'party_1_rating', 'party_1_adr',
|
||||
'party_2_win_rate', 'party_2_rating', 'party_2_adr',
|
||||
'party_3_win_rate', 'party_3_rating', 'party_3_adr',
|
||||
'party_4_win_rate', 'party_4_rating', 'party_4_adr',
|
||||
'party_5_win_rate', 'party_5_rating', 'party_5_adr',
|
||||
# New: Rating Distribution
|
||||
'rating_dist_carry_rate', 'rating_dist_normal_rate', 'rating_dist_sacrifice_rate', 'rating_dist_sleeping_rate',
|
||||
# New: ELO Stratification
|
||||
'elo_lt1200_rating', 'elo_1200_1400_rating', 'elo_1400_1600_rating', 'elo_1600_1800_rating', 'elo_1800_2000_rating', 'elo_gt2000_rating',
|
||||
# New: Clutch & Multi (Real Calculation)
|
||||
'clutch_rate_1v1', 'clutch_rate_1v2', 'clutch_rate_1v3', 'clutch_rate_1v4', 'clutch_rate_1v5',
|
||||
'multikill_rate_2k', 'multikill_rate_3k', 'multikill_rate_4k', 'multikill_rate_5k',
|
||||
'multiassist_rate_2a', 'multiassist_rate_3a', 'multiassist_rate_4a', 'multiassist_rate_5a',
|
||||
'total_multikill_rate', 'total_multiassist_rate'
|
||||
]
|
||||
|
||||
# Mapping for L2 legacy calls (if any) - mainly map 'rating' to 'basic_avg_rating' etc if needed
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<!-- Footer -->
|
||||
<footer class="bg-white dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700 mt-auto">
|
||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
<p class="text-center text-sm text-gray-500">© 2026 YRTV CS2 Data Platform. All rights reserved.</p>
|
||||
<p class="text-center text-sm text-gray-500">© 2026 YRTV Data Platform. All rights reserved. 赣ICP备2026001600号</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -194,97 +194,86 @@
|
||||
|
||||
<!-- Tab: Head to Head -->
|
||||
<div x-show="tab === 'h2h'" class="bg-white dark:bg-slate-800 shadow rounded-lg overflow-hidden p-6" style="display: none;">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Head-to-Head Kills</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="flex justify-between items-end mb-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Head-to-Head Matrix</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">Shows <span class="font-bold text-green-600 bg-green-50 px-1 rounded">Kills</span> : <span class="font-bold text-red-500 bg-red-50 px-1 rounded">Deaths</span> interaction between players</p>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 font-mono">
|
||||
Row: Team 1 Players<br>
|
||||
Col: Team 2 Players
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-xl border border-gray-200 dark:border-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-slate-700">
|
||||
<thead class="bg-gray-50 dark:bg-slate-700/50">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Killer \ Victim</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider bg-gray-50 dark:bg-slate-700/50 sticky left-0 z-10">
|
||||
Team 1 \ Team 2
|
||||
</th>
|
||||
{% for victim in team2_players %}
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-300 tracking-wider w-20" title="{{ victim.username }}">
|
||||
<div class="flex flex-col items-center">
|
||||
<th class="px-2 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-300 tracking-wider min-w-[80px]" title="{{ victim.username }}">
|
||||
<div class="flex flex-col items-center group">
|
||||
<div class="relative">
|
||||
{% if victim.avatar_url %}
|
||||
<img class="h-6 w-6 rounded-full mb-1" src="{{ victim.avatar_url }}">
|
||||
<img class="h-8 w-8 rounded-full mb-1 border-2 border-transparent group-hover:border-yrtv-400 transition-all" src="{{ victim.avatar_url }}">
|
||||
{% else %}
|
||||
<div class="h-6 w-6 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200 mb-1">
|
||||
<div class="h-8 w-8 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border-2 border-yrtv-200 mb-1 group-hover:border-yrtv-400 transition-all">
|
||||
{{ (victim.username or victim.steam_id_64)[:2] | upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="truncate w-16 text-center">{{ victim.username or 'Player' }}</span>
|
||||
</div>
|
||||
<span class="truncate w-20 text-center font-bold text-gray-700 dark:text-gray-300 group-hover:text-yrtv-600 transition-colors text-[10px]">{{ victim.username or 'Player' }}</span>
|
||||
</div>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{% for killer in team1_players %}
|
||||
<tr>
|
||||
<td class="px-3 py-2 whitespace-nowrap font-medium text-gray-900 dark:text-white flex items-center">
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/30 transition-colors">
|
||||
<td class="px-4 py-3 whitespace-nowrap font-medium text-gray-900 dark:text-white bg-white dark:bg-slate-800 sticky left-0 z-10 border-r border-gray-100 dark:border-gray-700 shadow-sm">
|
||||
<div class="flex items-center group">
|
||||
{% if killer.avatar_url %}
|
||||
<img class="h-6 w-6 rounded-full mr-2" src="{{ killer.avatar_url }}">
|
||||
<img class="h-8 w-8 rounded-full mr-3 border-2 border-transparent group-hover:border-blue-400 transition-all" src="{{ killer.avatar_url }}">
|
||||
{% else %}
|
||||
<div class="h-6 w-6 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200 mr-2">
|
||||
<div class="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold text-xs border-2 border-blue-200 mr-3 group-hover:border-blue-400 transition-all">
|
||||
{{ (killer.username or killer.steam_id_64)[:2] | upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="truncate w-24">{{ killer.username or 'Player' }}</span>
|
||||
<span class="truncate w-28 font-bold group-hover:text-blue-600 transition-colors">{{ killer.username or 'Player' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
{% for victim in team2_players %}
|
||||
<!-- Kills: Killer -> Victim -->
|
||||
{% set kills = h2h_matrix.get(killer.steam_id_64, {}).get(victim.steam_id_64, 0) %}
|
||||
<td class="px-3 py-2 text-center text-sm border-l border-gray-100 dark:border-gray-700
|
||||
{% if kills > 0 %}font-bold text-gray-900 dark:text-white{% else %}text-gray-300 dark:text-gray-600{% endif %}"
|
||||
style="{% if kills > 0 %}background-color: rgba(239, 68, 68, {{ kills * 0.1 }}){% endif %}">
|
||||
{{ kills if kills > 0 else '-' }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Deaths: Victim -> Killer (which is Killer's death) -->
|
||||
{% set deaths = h2h_matrix.get(victim.steam_id_64, {}).get(killer.steam_id_64, 0) %}
|
||||
|
||||
<td class="px-2 py-3 text-center border-l border-gray-50 dark:border-gray-700/50">
|
||||
<div class="flex items-center justify-center gap-1.5 font-mono">
|
||||
<!-- Kills -->
|
||||
<span class="{% if kills > deaths %}font-black text-lg text-green-600{% elif kills > 0 %}font-bold text-gray-900 dark:text-white{% else %}text-gray-300 dark:text-gray-600 text-xs{% endif %}">
|
||||
{{ kills }}
|
||||
</span>
|
||||
|
||||
<span class="text-gray-300 dark:text-gray-600 text-[10px]">:</span>
|
||||
|
||||
<!-- Deaths -->
|
||||
<span class="{% if deaths > kills %}font-black text-lg text-red-500{% elif deaths > 0 %}font-bold text-gray-900 dark:text-white{% else %}text-gray-300 dark:text-gray-600 text-xs{% endif %}">
|
||||
{{ deaths }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="my-6 border-t border-gray-200 dark:border-gray-700"></div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-slate-700">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Killer \ Victim</th>
|
||||
{% for victim in team1_players %}
|
||||
<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-300 tracking-wider w-20" title="{{ victim.username }}">
|
||||
<div class="flex flex-col items-center">
|
||||
{% if victim.avatar_url %}
|
||||
<img class="h-6 w-6 rounded-full mb-1" src="{{ victim.avatar_url }}">
|
||||
{% else %}
|
||||
<div class="h-6 w-6 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200 mb-1">
|
||||
{{ (victim.username or victim.steam_id_64)[:2] | upper }}
|
||||
<!-- Interaction Bar (Optional visual) -->
|
||||
{% if kills + deaths > 0 %}
|
||||
<div class="w-full h-1 bg-gray-100 dark:bg-slate-700 rounded-full mt-1 overflow-hidden flex">
|
||||
{% set total = kills + deaths %}
|
||||
<div class="bg-green-500 h-full" style="width: {{ (kills / total * 100) }}%"></div>
|
||||
<div class="bg-red-500 h-full" style="width: {{ (deaths / total * 100) }}%"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="truncate w-16 text-center">{{ victim.username or 'Player' }}</span>
|
||||
</div>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for killer in team2_players %}
|
||||
<tr>
|
||||
<td class="px-3 py-2 whitespace-nowrap font-medium text-gray-900 dark:text-white flex items-center">
|
||||
{% if killer.avatar_url %}
|
||||
<img class="h-6 w-6 rounded-full mr-2" src="{{ killer.avatar_url }}">
|
||||
{% else %}
|
||||
<div class="h-6 w-6 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200 mr-2">
|
||||
{{ (killer.username or killer.steam_id_64)[:2] | upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="truncate w-24">{{ killer.username or 'Player' }}</span>
|
||||
</td>
|
||||
{% for victim in team1_players %}
|
||||
{% set kills = h2h_matrix.get(killer.steam_id_64, {}).get(victim.steam_id_64, 0) %}
|
||||
<td class="px-3 py-2 text-center text-sm border-l border-gray-100 dark:border-gray-700
|
||||
{% if kills > 0 %}font-bold text-gray-900 dark:text-white{% else %}text-gray-300 dark:text-gray-600{% endif %}"
|
||||
style="{% if kills > 0 %}background-color: rgba(59, 130, 246, {{ kills * 0.1 }}){% endif %}">
|
||||
{{ kills if kills > 0 else '-' }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
||||
@@ -147,11 +147,11 @@
|
||||
<span>📊</span> 详细数据面板 (Detailed Stats)
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
|
||||
{% 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 %}
|
||||
<div class="flex flex-col group relative">
|
||||
<div class="flex flex-col group relative h-full">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="text-xs font-bold text-gray-400 uppercase tracking-wider">{{ label }}</span>
|
||||
<span class="text-xs font-bold text-gray-400 uppercase tracking-wider truncate" title="{{ label }}">{{ label }}</span>
|
||||
{% if dist %}
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-bold
|
||||
{% if dist.rank == 1 %}bg-yellow-50 text-yellow-700 border border-yellow-100
|
||||
@@ -162,7 +162,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex items-baseline gap-1 mb-1">
|
||||
<div class="flex justify-between items-end mb-1">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-xl font-black text-gray-900 dark:text-white font-mono">
|
||||
{{ format_str.format(value if value is not none else 0) }}
|
||||
</span>
|
||||
@@ -171,6 +172,13 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if count_label is not none %}
|
||||
<div class="text-[10px] font-bold text-gray-400 font-mono mb-0.5">
|
||||
{{ count_label }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Distribution Bar -->
|
||||
{% if dist %}
|
||||
<div class="w-full h-1 bg-gray-100 dark:bg-slate-700 rounded-full overflow-hidden relative mt-1">
|
||||
@@ -222,16 +230,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%}') }}
|
||||
|
||||
<!-- Row 6: Clutch -->
|
||||
{{ 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}') }}
|
||||
|
||||
<!-- Row 7: Special -->
|
||||
<!-- Row 6: Special -->
|
||||
{{ 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') }}
|
||||
</div>
|
||||
@@ -269,8 +269,8 @@
|
||||
HPS (Clutch/Pressure) & PTL (Pistol)
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
|
||||
{{ detail_item('1v1 Win% (1v1胜率)', features['hps_clutch_win_rate_1v1'], 'hps_clutch_win_rate_1v1', '{:.1%}') }}
|
||||
{{ detail_item('1v3+ Win% (残局大神)', features['hps_clutch_win_rate_1v3_plus'], 'hps_clutch_win_rate_1v3_plus', '{:.1%}') }}
|
||||
{{ detail_item('Avg 1v1 (场均1v1)', features['hps_clutch_win_rate_1v1'], 'hps_clutch_win_rate_1v1', '{:.2f}') }}
|
||||
{{ detail_item('Avg 1v3+ (场均1v3+)', features['hps_clutch_win_rate_1v3_plus'], 'hps_clutch_win_rate_1v3_plus', '{:.2f}') }}
|
||||
{{ detail_item('Match Pt Win% (赛点胜率)', features['hps_match_point_win_rate'], 'hps_match_point_win_rate', '{:.1%}') }}
|
||||
{{ detail_item('Pressure Entry (逆风首杀)', features['hps_pressure_entry_rate'], 'hps_pressure_entry_rate', '{:.1%}') }}
|
||||
{{ detail_item('Comeback KD (翻盘KD)', features['hps_comeback_kd_diff'], 'hps_comeback_kd_diff') }}
|
||||
@@ -296,15 +296,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 5: SPECIAL (Clutch & Multi) -->
|
||||
<div>
|
||||
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
|
||||
SPECIAL (Clutch & Multi)
|
||||
</h4>
|
||||
{% set matches = l2_stats.get('matches', 0) or 1 %}
|
||||
{% set rounds = l2_stats.get('total_rounds', 0) or 1 %}
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
|
||||
{% set c1 = l2_stats.get('c1', 0) or 0 %}
|
||||
{% set a1 = l2_stats.get('att1', 0) or 0 %}
|
||||
{{ detail_item('1v1 Win% (1v1胜率)', c1 / a1 if a1 > 0 else 0, 'clutch_rate_1v1', '{:.1%}', count_label=c1 ~ '/' ~ a1) }}
|
||||
|
||||
{% set c2 = l2_stats.get('c2', 0) or 0 %}
|
||||
{% set a2 = l2_stats.get('att2', 0) or 0 %}
|
||||
{{ detail_item('1v2 Win% (1v2胜率)', c2 / a2 if a2 > 0 else 0, 'clutch_rate_1v2', '{:.1%}', count_label=c2 ~ '/' ~ a2) }}
|
||||
|
||||
{% set c3 = l2_stats.get('c3', 0) or 0 %}
|
||||
{% set a3 = l2_stats.get('att3', 0) or 0 %}
|
||||
{{ detail_item('1v3 Win% (1v3胜率)', c3 / a3 if a3 > 0 else 0, 'clutch_rate_1v3', '{:.1%}', count_label=c3 ~ '/' ~ a3) }}
|
||||
|
||||
{% set c4 = l2_stats.get('c4', 0) or 0 %}
|
||||
{% set a4 = l2_stats.get('att4', 0) or 0 %}
|
||||
{{ detail_item('1v4 Win% (1v4胜率)', c4 / a4 if a4 > 0 else 0, 'clutch_rate_1v4', '{:.1%}', count_label=c4 ~ '/' ~ a4) }}
|
||||
|
||||
{% set c5 = l2_stats.get('c5', 0) or 0 %}
|
||||
{% set a5 = l2_stats.get('att5', 0) or 0 %}
|
||||
{{ detail_item('1v5 Win% (1v5胜率)', c5 / a5 if a5 > 0 else 0, 'clutch_rate_1v5', '{:.1%}', count_label=c5 ~ '/' ~ a5) }}
|
||||
|
||||
{% 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-K Rate (多杀率)', mk_count / rounds, 'total_multikill_rate', '{:.1%}', count_label=mk_count) }}
|
||||
{{ detail_item('Multi-A Rate (多助率)', ma_count / rounds, 'total_multiassist_rate', '{:.1%}', count_label=ma_count) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 4: SIDE (T/CT Preference) -->
|
||||
<div>
|
||||
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
|
||||
SIDE (T/CT Preference)
|
||||
</h4>
|
||||
|
||||
{% 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 +401,10 @@
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{{ vs_item('Rating (Rating/KD)', 'side_rating_t', 'side_rating_ct') }}
|
||||
{{ vs_item('KD Ratio', 'side_kd_t', 'side_kd_ct') }}
|
||||
@@ -375,8 +413,82 @@
|
||||
{{ 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%}') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Section: Party & Stratification -->
|
||||
<div>
|
||||
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
|
||||
👥 组排与分层表现 (Party & Stratification)
|
||||
</h4>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Group 1: Party Size -->
|
||||
<div>
|
||||
<h5 class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-3">Party Size Performance (组排表现)</h5>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
|
||||
{{ detail_item('Solo Win% (单排胜率)', features['party_1_win_rate'], 'party_1_win_rate', '{:.1%}') }}
|
||||
{{ detail_item('Solo Rating (单排分)', features['party_1_rating'], 'party_1_rating') }}
|
||||
{{ detail_item('Solo ADR (单排伤)', features['party_1_adr'], 'party_1_adr', '{:.1f}') }}
|
||||
|
||||
{{ detail_item('Duo Win% (双排胜率)', features['party_2_win_rate'], 'party_2_win_rate', '{:.1%}') }}
|
||||
{{ detail_item('Duo Rating (双排分)', features['party_2_rating'], 'party_2_rating') }}
|
||||
{{ detail_item('Duo ADR (双排伤)', features['party_2_adr'], 'party_2_adr', '{:.1f}') }}
|
||||
|
||||
{{ detail_item('Trio Win% (三排胜率)', features['party_3_win_rate'], 'party_3_win_rate', '{:.1%}') }}
|
||||
{{ detail_item('Trio Rating (三排分)', features['party_3_rating'], 'party_3_rating') }}
|
||||
{{ detail_item('Trio ADR (三排伤)', features['party_3_adr'], 'party_3_adr', '{:.1f}') }}
|
||||
|
||||
{{ detail_item('Quad Win% (四排胜率)', features['party_4_win_rate'], 'party_4_win_rate', '{:.1%}') }}
|
||||
{{ detail_item('Quad Rating (四排分)', features['party_4_rating'], 'party_4_rating') }}
|
||||
{{ detail_item('Quad ADR (四排伤)', features['party_4_adr'], 'party_4_adr', '{:.1f}') }}
|
||||
|
||||
{{ detail_item('Full Win% (五排胜率)', features['party_5_win_rate'], 'party_5_win_rate', '{:.1%}') }}
|
||||
{{ detail_item('Full Rating (五排分)', features['party_5_rating'], 'party_5_rating') }}
|
||||
{{ detail_item('Full ADR (五排伤)', features['party_5_adr'], 'party_5_adr', '{:.1f}') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 2: Rating Distribution -->
|
||||
<div>
|
||||
<h5 class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-3">Performance Tiers (表现分层)</h5>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-y-6 gap-x-4">
|
||||
{{ detail_item('Carry Rate (>1.5)', features['rating_dist_carry_rate'], 'rating_dist_carry_rate', '{:.1%}') }}
|
||||
{{ detail_item('Normal Rate (1.0-1.5)', features['rating_dist_normal_rate'], 'rating_dist_normal_rate', '{:.1%}') }}
|
||||
{{ detail_item('Sacrifice Rate (0.6-1.0)', features['rating_dist_sacrifice_rate'], 'rating_dist_sacrifice_rate', '{:.1%}') }}
|
||||
{{ detail_item('Sleeping Rate (<0.6)', features['rating_dist_sleeping_rate'], 'rating_dist_sleeping_rate', '{:.1%}') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 3: ELO Stratification -->
|
||||
<div>
|
||||
<h5 class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-3">Performance vs ELO (不同分段表现)</h5>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-y-6 gap-x-4">
|
||||
{{ detail_item('<1200 Rating', features['elo_lt1200_rating'], 'elo_lt1200_rating') }}
|
||||
{{ detail_item('1200-1400 Rating', features['elo_1200_1400_rating'], 'elo_1200_1400_rating') }}
|
||||
{{ detail_item('1400-1600 Rating', features['elo_1400_1600_rating'], 'elo_1400_1600_rating') }}
|
||||
{{ detail_item('1600-1800 Rating', features['elo_1600_1800_rating'], 'elo_1600_1800_rating') }}
|
||||
{{ detail_item('1800-2000 Rating', features['elo_1800_2000_rating'], 'elo_1800_2000_rating') }}
|
||||
{{ detail_item('>2000 Rating', features['elo_gt2000_rating'], 'elo_gt2000_rating') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user