3.0.0 : Reconstructed Database System.
This commit is contained in:
@@ -493,9 +493,24 @@ class StatsService:
|
||||
|
||||
@staticmethod
|
||||
def get_player_basic_stats(steam_id):
|
||||
# Calculate stats from fact_match_players
|
||||
# Prefer calculating from sums (kills/deaths) for K/D accuracy
|
||||
# AVG(adr) is used as damage_total might be missing in some sources
|
||||
l3 = query_db(
|
||||
"l3",
|
||||
"""
|
||||
SELECT
|
||||
total_matches as matches_played,
|
||||
core_avg_rating as rating,
|
||||
core_avg_kd as kd,
|
||||
core_avg_kast as kast,
|
||||
core_avg_adr as adr
|
||||
FROM dm_player_features
|
||||
WHERE steam_id_64 = ?
|
||||
""",
|
||||
[steam_id],
|
||||
one=True,
|
||||
)
|
||||
if l3 and (l3["matches_played"] or 0) > 0:
|
||||
return dict(l3)
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
AVG(rating) as rating,
|
||||
@@ -508,28 +523,20 @@ class StatsService:
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
"""
|
||||
row = query_db('l2', sql, [steam_id], one=True)
|
||||
|
||||
if row and row['matches_played'] > 0:
|
||||
row = query_db("l2", sql, [steam_id], one=True)
|
||||
|
||||
if row and row["matches_played"] > 0:
|
||||
res = dict(row)
|
||||
|
||||
# Calculate K/D: Sum Kills / Sum Deaths
|
||||
kills = res.get('total_kills') or 0
|
||||
deaths = res.get('total_deaths') or 0
|
||||
|
||||
kills = res.get("total_kills") or 0
|
||||
deaths = res.get("total_deaths") or 0
|
||||
if deaths > 0:
|
||||
res['kd'] = kills / deaths
|
||||
res["kd"] = kills / deaths
|
||||
else:
|
||||
res['kd'] = kills # If 0 deaths, K/D is kills (or infinity, but kills is safer for display)
|
||||
|
||||
# Fallback to avg_kd if calculation failed (e.g. both 0) but avg_kd exists
|
||||
if res['kd'] == 0 and res['avg_kd'] and res['avg_kd'] > 0:
|
||||
res['kd'] = res['avg_kd']
|
||||
|
||||
# ADR validation
|
||||
if res['adr'] is None:
|
||||
res['adr'] = 0.0
|
||||
|
||||
res["kd"] = kills
|
||||
if res["kd"] == 0 and res["avg_kd"] and res["avg_kd"] > 0:
|
||||
res["kd"] = res["avg_kd"]
|
||||
if res["adr"] is None:
|
||||
res["adr"] = 0.0
|
||||
return res
|
||||
return None
|
||||
|
||||
@@ -599,8 +606,30 @@ class StatsService:
|
||||
|
||||
@staticmethod
|
||||
def get_player_trend(steam_id, limit=20):
|
||||
# We need party_size: count of players with same match_team_id in the same match
|
||||
# Using a correlated subquery for party_size
|
||||
l3_sql = """
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
match_date as start_time,
|
||||
rating,
|
||||
kd_ratio,
|
||||
adr,
|
||||
kast,
|
||||
match_id,
|
||||
map_name,
|
||||
is_win,
|
||||
match_sequence as match_index
|
||||
FROM dm_player_match_history
|
||||
WHERE steam_id_64 = ?
|
||||
ORDER BY match_date DESC
|
||||
LIMIT ?
|
||||
)
|
||||
ORDER BY start_time ASC
|
||||
"""
|
||||
l3_rows = query_db("l3", l3_sql, [steam_id, limit])
|
||||
if l3_rows:
|
||||
return l3_rows
|
||||
|
||||
sql = """
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
@@ -616,7 +645,7 @@ class StatsService:
|
||||
FROM fact_match_players p2
|
||||
WHERE p2.match_id = mp.match_id
|
||||
AND p2.match_team_id = mp.match_team_id
|
||||
AND p2.match_team_id > 0 -- Ensure we don't count 0 (solo default) as a massive party
|
||||
AND p2.match_team_id > 0
|
||||
) as party_size,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
@@ -630,7 +659,7 @@ class StatsService:
|
||||
LIMIT ?
|
||||
) ORDER BY start_time ASC
|
||||
"""
|
||||
return query_db('l2', sql, [steam_id, limit])
|
||||
return query_db("l2", sql, [steam_id, limit])
|
||||
|
||||
@staticmethod
|
||||
def get_recent_performance_stats(steam_id):
|
||||
@@ -639,63 +668,59 @@ class StatsService:
|
||||
- Last 5, 10, 15 matches
|
||||
- Last 5, 10, 15 days
|
||||
"""
|
||||
import numpy as np
|
||||
from datetime import datetime, timedelta
|
||||
def avg_var(nums):
|
||||
if not nums:
|
||||
return 0.0, 0.0
|
||||
n = len(nums)
|
||||
avg = sum(nums) / n
|
||||
var = sum((x - avg) ** 2 for x in nums) / n
|
||||
return avg, var
|
||||
|
||||
rows = query_db(
|
||||
"l3",
|
||||
"""
|
||||
SELECT match_date as t, rating
|
||||
FROM dm_player_match_history
|
||||
WHERE steam_id_64 = ?
|
||||
ORDER BY match_date DESC
|
||||
""",
|
||||
[steam_id],
|
||||
)
|
||||
if not rows:
|
||||
rows = query_db(
|
||||
"l2",
|
||||
"""
|
||||
SELECT m.start_time as t, mp.rating
|
||||
FROM fact_match_players mp
|
||||
JOIN fact_matches m ON mp.match_id = m.match_id
|
||||
WHERE mp.steam_id_64 = ?
|
||||
ORDER BY m.start_time DESC
|
||||
""",
|
||||
[steam_id],
|
||||
)
|
||||
|
||||
# Fetch all match ratings with timestamps
|
||||
sql = """
|
||||
SELECT m.start_time, mp.rating
|
||||
FROM fact_match_players mp
|
||||
JOIN fact_matches m ON mp.match_id = m.match_id
|
||||
WHERE mp.steam_id_64 = ?
|
||||
ORDER BY m.start_time DESC
|
||||
"""
|
||||
rows = query_db('l2', sql, [steam_id])
|
||||
|
||||
if not rows:
|
||||
return {}
|
||||
|
||||
# Convert to list of dicts
|
||||
matches = [{'time': r['start_time'], 'rating': r['rating'] or 0} for r in rows]
|
||||
|
||||
matches = [{"time": r["t"], "rating": float(r["rating"] or 0)} for r in rows]
|
||||
stats = {}
|
||||
|
||||
# 1. Recent N Matches
|
||||
|
||||
for n in [5, 10, 15]:
|
||||
subset = matches[:n]
|
||||
if not subset:
|
||||
stats[f'last_{n}_matches'] = {'avg': 0, 'var': 0, 'count': 0}
|
||||
continue
|
||||
|
||||
ratings = [m['rating'] for m in subset]
|
||||
stats[f'last_{n}_matches'] = {
|
||||
'avg': np.mean(ratings),
|
||||
'var': np.var(ratings),
|
||||
'count': len(ratings)
|
||||
}
|
||||
ratings = [m["rating"] for m in subset]
|
||||
avg, var = avg_var(ratings)
|
||||
stats[f"last_{n}_matches"] = {"avg": avg, "var": var, "count": len(ratings)}
|
||||
|
||||
# 2. Recent N Days
|
||||
# Use server time or max match time? usually server time 'now' is fine if data is fresh.
|
||||
# But if data is old, 'last 5 days' might be empty.
|
||||
# User asked for "recent 5/10/15 days", implying calendar days from now.
|
||||
import time
|
||||
|
||||
now = time.time()
|
||||
|
||||
for d in [5, 10, 15]:
|
||||
cutoff = now - (d * 24 * 3600)
|
||||
subset = [m for m in matches if m['time'] >= cutoff]
|
||||
|
||||
if not subset:
|
||||
stats[f'last_{d}_days'] = {'avg': 0, 'var': 0, 'count': 0}
|
||||
continue
|
||||
|
||||
ratings = [m['rating'] for m in subset]
|
||||
stats[f'last_{d}_days'] = {
|
||||
'avg': np.mean(ratings),
|
||||
'var': np.var(ratings),
|
||||
'count': len(ratings)
|
||||
}
|
||||
|
||||
subset = [m for m in matches if (m["time"] or 0) >= cutoff]
|
||||
ratings = [m["rating"] for m in subset]
|
||||
avg, var = avg_var(ratings)
|
||||
stats[f"last_{d}_days"] = {"avg": avg, "var": var, "count": len(ratings)}
|
||||
|
||||
return stats
|
||||
|
||||
@staticmethod
|
||||
@@ -707,7 +732,6 @@ class StatsService:
|
||||
from web.services.web_service import WebService
|
||||
from web.services.feature_service import FeatureService
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
# 1. Get Active Roster IDs
|
||||
lineups = WebService.get_lineups()
|
||||
@@ -722,136 +746,141 @@ class StatsService:
|
||||
if not active_roster_ids:
|
||||
return None
|
||||
|
||||
# 2. Fetch L3 features for all roster members
|
||||
# We need to use FeatureService to get the full L3 set (including detailed stats)
|
||||
# Assuming L3 data is up to date.
|
||||
|
||||
placeholders = ','.join('?' for _ in active_roster_ids)
|
||||
sql = f"SELECT * FROM dm_player_features WHERE steam_id_64 IN ({placeholders})"
|
||||
rows = query_db('l3', sql, active_roster_ids)
|
||||
|
||||
placeholders = ",".join("?" for _ in active_roster_ids)
|
||||
rows = query_db("l3", f"SELECT * FROM dm_player_features WHERE steam_id_64 IN ({placeholders})", active_roster_ids)
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
stats_map = {row['steam_id_64']: dict(row) for row in rows}
|
||||
|
||||
stats_map = {str(row["steam_id_64"]): FeatureService._normalize_features(dict(row)) for row in rows}
|
||||
target_steam_id = str(target_steam_id)
|
||||
|
||||
# If target not in map (e.g. no L3 data), try to add empty default
|
||||
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 = [
|
||||
'basic_avg_rating', 'basic_avg_kd', 'basic_avg_kast', 'basic_avg_rws', 'basic_avg_adr',
|
||||
'basic_avg_headshot_kills', 'basic_headshot_rate', 'basic_avg_assisted_kill', 'basic_avg_awp_kill', 'basic_avg_jump_count',
|
||||
'basic_avg_knife_kill', 'basic_avg_zeus_kill', 'basic_zeus_pick_rate',
|
||||
'basic_avg_mvps', 'basic_avg_plants', 'basic_avg_defuses', 'basic_avg_flash_assists',
|
||||
'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_perfect_kill', 'basic_avg_revenge_kill',
|
||||
# L3 Advanced Dimensions
|
||||
'sta_last_30_rating', 'sta_win_rating', 'sta_loss_rating', 'sta_rating_volatility', 'sta_time_rating_corr',
|
||||
'bat_kd_diff_high_elo', 'bat_avg_duel_win_rate', 'bat_win_rate_vs_all',
|
||||
'hps_clutch_win_rate_1v1', 'hps_clutch_win_rate_1v3_plus', 'hps_match_point_win_rate', 'hps_pressure_entry_rate', 'hps_comeback_kd_diff', 'hps_losing_streak_kd_diff',
|
||||
'ptl_pistol_kills', 'ptl_pistol_win_rate', 'ptl_pistol_kd', 'ptl_pistol_util_efficiency',
|
||||
'side_rating_ct', 'side_rating_t', 'side_first_kill_rate_ct', 'side_first_kill_rate_t', 'side_kd_diff_ct_t', 'side_hold_success_rate_ct', 'side_entry_success_rate_t',
|
||||
'side_win_rate_ct', 'side_win_rate_t', 'side_kd_ct', 'side_kd_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',
|
||||
'util_avg_nade_dmg', 'util_avg_flash_time', 'util_avg_flash_enemy', 'util_usage_rate',
|
||||
# New: ECO & PACE
|
||||
'eco_avg_damage_per_1k', 'eco_rating_eco_rounds', 'eco_kd_ratio', 'eco_avg_rounds',
|
||||
'pace_avg_time_to_first_contact', 'pace_trade_kill_rate', 'pace_opening_kill_time', 'pace_avg_life_time',
|
||||
# New: ROUND (Round Dynamics)
|
||||
'rd_phase_kill_early_share', 'rd_phase_kill_mid_share', 'rd_phase_kill_late_share',
|
||||
'rd_phase_death_early_share', 'rd_phase_death_mid_share', 'rd_phase_death_late_share',
|
||||
'rd_phase_kill_early_share_t', 'rd_phase_kill_mid_share_t', 'rd_phase_kill_late_share_t',
|
||||
'rd_phase_kill_early_share_ct', 'rd_phase_kill_mid_share_ct', 'rd_phase_kill_late_share_ct',
|
||||
'rd_phase_death_early_share_t', 'rd_phase_death_mid_share_t', 'rd_phase_death_late_share_t',
|
||||
'rd_phase_death_early_share_ct', 'rd_phase_death_mid_share_ct', 'rd_phase_death_late_share_ct',
|
||||
'rd_firstdeath_team_first_death_win_rate', 'rd_invalid_death_rate',
|
||||
'rd_pressure_kpr_ratio', 'rd_matchpoint_kpr_ratio', 'rd_trade_response_10s_rate',
|
||||
'rd_pressure_perf_ratio', 'rd_matchpoint_perf_ratio',
|
||||
'rd_comeback_kill_share', 'map_stability_coef',
|
||||
# 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'
|
||||
# TIER 1: CORE
|
||||
# Basic Performance
|
||||
"core_avg_rating", "core_avg_rating2", "core_avg_kd", "core_avg_adr", "core_avg_kast",
|
||||
"core_avg_rws", "core_avg_hs_kills", "core_hs_rate", "core_total_kills", "core_total_deaths",
|
||||
"core_total_assists", "core_avg_assists", "core_kpr", "core_dpr", "core_survival_rate",
|
||||
# Match Stats
|
||||
"core_win_rate", "core_wins", "core_losses", "core_avg_match_duration", "core_avg_mvps",
|
||||
"core_mvp_rate", "core_avg_elo_change", "core_total_elo_gained",
|
||||
# Weapon Stats
|
||||
"core_avg_awp_kills", "core_awp_usage_rate", "core_avg_knife_kills", "core_avg_zeus_kills",
|
||||
"core_zeus_buy_rate", "core_top_weapon_kills", "core_top_weapon_hs_rate",
|
||||
"core_weapon_diversity", "core_rifle_hs_rate", "core_pistol_hs_rate", "core_smg_kills_total",
|
||||
# Objective Stats
|
||||
"core_avg_plants", "core_avg_defuses", "core_avg_flash_assists", "core_plant_success_rate",
|
||||
"core_defuse_success_rate", "core_objective_impact",
|
||||
|
||||
# TIER 2: TACTICAL
|
||||
# Opening Impact
|
||||
"tac_avg_fk", "tac_avg_fd", "tac_fk_rate", "tac_fd_rate", "tac_fk_success_rate",
|
||||
"tac_entry_kill_rate", "tac_entry_death_rate", "tac_opening_duel_winrate",
|
||||
# Multi-Kill
|
||||
"tac_avg_2k", "tac_avg_3k", "tac_avg_4k", "tac_avg_5k", "tac_multikill_rate", "tac_ace_count",
|
||||
# Clutch Performance
|
||||
"tac_clutch_1v1_attempts", "tac_clutch_1v1_wins", "tac_clutch_1v1_rate",
|
||||
"tac_clutch_1v2_attempts", "tac_clutch_1v2_wins", "tac_clutch_1v2_rate",
|
||||
"tac_clutch_1v3_plus_attempts", "tac_clutch_1v3_plus_wins", "tac_clutch_1v3_plus_rate",
|
||||
"tac_clutch_impact_score",
|
||||
# Utility Mastery
|
||||
"tac_util_flash_per_round", "tac_util_smoke_per_round", "tac_util_molotov_per_round",
|
||||
"tac_util_he_per_round", "tac_util_usage_rate", "tac_util_nade_dmg_per_round",
|
||||
"tac_util_nade_dmg_per_nade", "tac_util_flash_time_per_round", "tac_util_flash_enemies_per_round",
|
||||
"tac_util_flash_efficiency", "tac_util_smoke_timing_score", "tac_util_impact_score",
|
||||
# Economy Efficiency
|
||||
"tac_eco_dmg_per_1k", "tac_eco_kpr_eco_rounds", "tac_eco_kd_eco_rounds",
|
||||
"tac_eco_kpr_force_rounds", "tac_eco_kpr_full_rounds", "tac_eco_save_discipline",
|
||||
"tac_eco_force_success_rate", "tac_eco_efficiency_score",
|
||||
|
||||
# TIER 3: INTELLIGENCE
|
||||
# High IQ Kills
|
||||
"int_wallbang_kills", "int_wallbang_rate", "int_smoke_kills", "int_smoke_kill_rate",
|
||||
"int_blind_kills", "int_blind_kill_rate", "int_noscope_kills", "int_noscope_rate", "int_high_iq_score",
|
||||
# Timing Analysis
|
||||
"int_timing_early_kills", "int_timing_mid_kills", "int_timing_late_kills",
|
||||
"int_timing_early_kill_share", "int_timing_mid_kill_share", "int_timing_late_kill_share",
|
||||
"int_timing_avg_kill_time", "int_timing_early_deaths", "int_timing_early_death_rate",
|
||||
"int_timing_aggression_index", "int_timing_patience_score", "int_timing_first_contact_time",
|
||||
# Pressure Performance
|
||||
"int_pressure_comeback_kd", "int_pressure_comeback_rating", "int_pressure_losing_streak_kd",
|
||||
"int_pressure_matchpoint_kpr", "int_pressure_matchpoint_rating", "int_pressure_clutch_composure",
|
||||
"int_pressure_entry_in_loss", "int_pressure_performance_index", "int_pressure_big_moment_score",
|
||||
"int_pressure_tilt_resistance",
|
||||
# Position Mastery
|
||||
"int_pos_site_a_control_rate", "int_pos_site_b_control_rate", "int_pos_mid_control_rate",
|
||||
"int_pos_position_diversity", "int_pos_rotation_speed", "int_pos_map_coverage",
|
||||
"int_pos_lurk_tendency", "int_pos_site_anchor_score", "int_pos_entry_route_diversity",
|
||||
"int_pos_retake_positioning", "int_pos_postplant_positioning", "int_pos_spatial_iq_score",
|
||||
"int_pos_avg_distance_from_teammates",
|
||||
# Trade Network
|
||||
"int_trade_kill_count", "int_trade_kill_rate", "int_trade_response_time",
|
||||
"int_trade_given_count", "int_trade_given_rate", "int_trade_balance",
|
||||
"int_trade_efficiency", "int_teamwork_score",
|
||||
|
||||
# TIER 4: META
|
||||
# Stability
|
||||
"meta_rating_volatility", "meta_recent_form_rating", "meta_win_rating", "meta_loss_rating",
|
||||
"meta_rating_consistency", "meta_time_rating_correlation", "meta_map_stability", "meta_elo_tier_stability",
|
||||
# Side Preference
|
||||
"meta_side_ct_rating", "meta_side_t_rating", "meta_side_ct_kd", "meta_side_t_kd",
|
||||
"meta_side_ct_win_rate", "meta_side_t_win_rate", "meta_side_ct_fk_rate", "meta_side_t_fk_rate",
|
||||
"meta_side_ct_kast", "meta_side_t_kast", "meta_side_rating_diff", "meta_side_kd_diff",
|
||||
"meta_side_balance_score",
|
||||
# Opponent Adaptation
|
||||
"meta_opp_vs_lower_elo_rating", "meta_opp_vs_similar_elo_rating", "meta_opp_vs_higher_elo_rating",
|
||||
"meta_opp_vs_lower_elo_kd", "meta_opp_vs_similar_elo_kd", "meta_opp_vs_higher_elo_kd",
|
||||
"meta_opp_elo_adaptation", "meta_opp_stomping_score", "meta_opp_upset_score",
|
||||
"meta_opp_consistency_across_elos", "meta_opp_rank_resistance", "meta_opp_smurf_detection",
|
||||
# Map Specialization
|
||||
"meta_map_best_rating", "meta_map_worst_rating", "meta_map_diversity", "meta_map_pool_size",
|
||||
"meta_map_specialist_score", "meta_map_versatility", "meta_map_comfort_zone_rate", "meta_map_adaptation",
|
||||
# Session Pattern
|
||||
"meta_session_avg_matches_per_day", "meta_session_longest_streak", "meta_session_weekend_rating",
|
||||
"meta_session_weekday_rating", "meta_session_morning_rating", "meta_session_afternoon_rating",
|
||||
"meta_session_evening_rating", "meta_session_night_rating",
|
||||
|
||||
# TIER 5: COMPOSITE
|
||||
"score_aim", "score_clutch", "score_pistol", "score_defense", "score_utility",
|
||||
"score_stability", "score_economy", "score_pace", "score_overall", "tier_percentile",
|
||||
|
||||
# Legacy Mappings (keep for compatibility if needed, or remove if fully migrated)
|
||||
"basic_avg_rating", "basic_avg_kd", "basic_avg_adr", "basic_avg_kast", "basic_avg_rws",
|
||||
]
|
||||
|
||||
# Mapping for L2 legacy calls (if any) - mainly map 'rating' to 'basic_avg_rating' etc if needed
|
||||
# But here we just use L3 columns directly.
|
||||
|
||||
# Define metrics where LOWER is BETTER
|
||||
lower_is_better = ['pace_avg_time_to_first_contact', 'pace_opening_kill_time', 'rd_invalid_death_rate', 'map_stability_coef']
|
||||
lower_is_better = []
|
||||
|
||||
result = {}
|
||||
|
||||
for m in metrics:
|
||||
values = [p.get(m, 0) or 0 for p in stats_map.values()]
|
||||
target_val = stats_map[target_steam_id].get(m, 0) or 0
|
||||
values = []
|
||||
non_numeric = False
|
||||
for p in stats_map.values():
|
||||
raw = (p or {}).get(m)
|
||||
if raw is None:
|
||||
raw = 0
|
||||
try:
|
||||
values.append(float(raw))
|
||||
except Exception:
|
||||
non_numeric = True
|
||||
break
|
||||
|
||||
raw_target = (stats_map.get(target_steam_id) or {}).get(m)
|
||||
if raw_target is None:
|
||||
raw_target = 0
|
||||
try:
|
||||
target_val = float(raw_target)
|
||||
except Exception:
|
||||
non_numeric = True
|
||||
target_val = 0
|
||||
|
||||
if non_numeric:
|
||||
result[m] = None
|
||||
continue
|
||||
|
||||
if not values:
|
||||
result[m] = None
|
||||
continue
|
||||
@@ -876,151 +905,15 @@ class StatsService:
|
||||
'inverted': not is_reverse # Flag for frontend to invert bar
|
||||
}
|
||||
|
||||
# Legacy mapping for top cards (rating, kd, adr, kast)
|
||||
legacy_map = {
|
||||
'basic_avg_rating': 'rating',
|
||||
'basic_avg_kd': 'kd',
|
||||
'basic_avg_adr': 'adr',
|
||||
'basic_avg_kast': 'kast'
|
||||
"basic_avg_rating": "rating",
|
||||
"basic_avg_kd": "kd",
|
||||
"basic_avg_adr": "adr",
|
||||
"basic_avg_kast": "kast",
|
||||
}
|
||||
if m in legacy_map:
|
||||
result[legacy_map[m]] = result[m]
|
||||
|
||||
def build_roundtype_metric_distribution(metric_key, round_type, subkey):
|
||||
values2 = []
|
||||
for sid, p in stats_map.items():
|
||||
raw = p.get('rd_roundtype_split_json') or ''
|
||||
if not raw:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(raw) if isinstance(raw, str) else raw
|
||||
except:
|
||||
continue
|
||||
if not isinstance(obj, dict):
|
||||
continue
|
||||
bucket = obj.get(round_type)
|
||||
if not isinstance(bucket, dict):
|
||||
continue
|
||||
v = bucket.get(subkey)
|
||||
if v is None:
|
||||
continue
|
||||
try:
|
||||
v = float(v)
|
||||
except:
|
||||
continue
|
||||
values2.append(v)
|
||||
raw_target = stats_map.get(target_steam_id, {}).get('rd_roundtype_split_json') or ''
|
||||
target_val2 = None
|
||||
if raw_target:
|
||||
try:
|
||||
obj_t = json.loads(raw_target) if isinstance(raw_target, str) else raw_target
|
||||
if isinstance(obj_t, dict) and isinstance(obj_t.get(round_type), dict):
|
||||
tv = obj_t[round_type].get(subkey)
|
||||
if tv is not None:
|
||||
target_val2 = float(tv)
|
||||
except:
|
||||
target_val2 = None
|
||||
if not values2 or target_val2 is None:
|
||||
return None
|
||||
values2.sort(reverse=True)
|
||||
try:
|
||||
rank2 = values2.index(target_val2) + 1
|
||||
except ValueError:
|
||||
rank2 = len(values2)
|
||||
return {
|
||||
'val': target_val2,
|
||||
'rank': rank2,
|
||||
'total': len(values2),
|
||||
'min': min(values2),
|
||||
'max': max(values2),
|
||||
'avg': sum(values2) / len(values2),
|
||||
'inverted': False
|
||||
}
|
||||
|
||||
rt_kpr_types = ['pistol', 'reg', 'overtime']
|
||||
rt_perf_types = ['eco', 'rifle', 'fullbuy', 'overtime']
|
||||
for t in rt_kpr_types:
|
||||
result[f'rd_rt_kpr_{t}'] = build_roundtype_metric_distribution('rd_roundtype_split_json', t, 'kpr')
|
||||
for t in rt_perf_types:
|
||||
result[f'rd_rt_perf_{t}'] = build_roundtype_metric_distribution('rd_roundtype_split_json', t, 'perf')
|
||||
|
||||
top_weapon_rank_map = {}
|
||||
try:
|
||||
raw_tw = stats_map.get(target_steam_id, {}).get('rd_weapon_top_json') or '[]'
|
||||
tw_items = json.loads(raw_tw) if isinstance(raw_tw, str) else raw_tw
|
||||
weapons = []
|
||||
if isinstance(tw_items, list):
|
||||
for it in tw_items:
|
||||
if isinstance(it, dict) and it.get('weapon'):
|
||||
weapons.append(str(it.get('weapon')))
|
||||
weapons = weapons[:5]
|
||||
except Exception:
|
||||
weapons = []
|
||||
|
||||
if weapons:
|
||||
w_placeholders = ','.join('?' for _ in weapons)
|
||||
sql_w = f"""
|
||||
SELECT attacker_steam_id as steam_id_64,
|
||||
weapon,
|
||||
COUNT(*) as kills,
|
||||
SUM(is_headshot) as hs
|
||||
FROM fact_round_events
|
||||
WHERE event_type='kill'
|
||||
AND attacker_steam_id IN ({l2_placeholders})
|
||||
AND weapon IN ({w_placeholders})
|
||||
GROUP BY attacker_steam_id, weapon
|
||||
"""
|
||||
weapon_rows = query_db('l2', sql_w, active_roster_ids + weapons)
|
||||
per_weapon = {}
|
||||
for r in weapon_rows:
|
||||
sid = str(r['steam_id_64'])
|
||||
w = str(r['weapon'] or '')
|
||||
if not w:
|
||||
continue
|
||||
kills = int(r['kills'] or 0)
|
||||
hs = int(r['hs'] or 0)
|
||||
mp = stats_map.get(sid, {}).get('total_matches') or 0
|
||||
try:
|
||||
mp = float(mp)
|
||||
except Exception:
|
||||
mp = 0
|
||||
kpm = (kills / mp) if (kills > 0 and mp > 0) else None
|
||||
hs_rate = (hs / kills) if kills > 0 else None
|
||||
per_weapon.setdefault(w, {})[sid] = {"kpm": kpm, "hs_rate": hs_rate}
|
||||
|
||||
for w in weapons:
|
||||
d = per_weapon.get(w) or {}
|
||||
target_d = d.get(target_steam_id) or {}
|
||||
target_kpm = target_d.get("kpm")
|
||||
target_hs = target_d.get("hs_rate")
|
||||
|
||||
kpm_vals = [v.get("kpm") for v in d.values() if v.get("kpm") is not None]
|
||||
hs_vals = [v.get("hs_rate") for v in d.values() if v.get("hs_rate") is not None]
|
||||
|
||||
kpm_rank = None
|
||||
hs_rank = None
|
||||
if kpm_vals and target_kpm is not None:
|
||||
kpm_vals.sort(reverse=True)
|
||||
try:
|
||||
kpm_rank = kpm_vals.index(target_kpm) + 1
|
||||
except ValueError:
|
||||
kpm_rank = len(kpm_vals)
|
||||
if hs_vals and target_hs is not None:
|
||||
hs_vals.sort(reverse=True)
|
||||
try:
|
||||
hs_rank = hs_vals.index(target_hs) + 1
|
||||
except ValueError:
|
||||
hs_rank = len(hs_vals)
|
||||
|
||||
top_weapon_rank_map[w] = {
|
||||
"kpm_rank": kpm_rank,
|
||||
"kpm_total": len(kpm_vals),
|
||||
"hs_rank": hs_rank,
|
||||
"hs_total": len(hs_vals),
|
||||
}
|
||||
|
||||
result['top_weapon_rank_map'] = top_weapon_rank_map
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user