2.1 : New

This commit is contained in:
2026-01-28 01:20:26 +08:00
parent b3941cad3b
commit 4afb728bfa
12 changed files with 1084 additions and 16 deletions

View File

@@ -725,6 +725,7 @@ class StatsService:
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',
@@ -745,6 +746,13 @@ class StatsService:
# 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_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',
@@ -766,7 +774,7 @@ class StatsService:
# 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']
lower_is_better = ['pace_avg_time_to_first_contact', 'pace_opening_kill_time', 'rd_invalid_death_rate', 'map_stability_coef']
result = {}
@@ -808,6 +816,141 @@ class StatsService:
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