3.0.0 : Reconstructed Database System.
This commit is contained in:
722
database/L3/processors/tactical_processor.py
Normal file
722
database/L3/processors/tactical_processor.py
Normal file
@@ -0,0 +1,722 @@
|
||||
"""
|
||||
TacticalProcessor - Tier 2: TACTICAL Features (44 columns)
|
||||
|
||||
Calculates tactical gameplay features from fact_match_players and fact_round_events:
|
||||
- Opening Impact (8 columns): first kills/deaths, entry duels
|
||||
- Multi-Kill Performance (6 columns): 2k, 3k, 4k, 5k, ace
|
||||
- Clutch Performance (10 columns): 1v1, 1v2, 1v3+ situations
|
||||
- Utility Mastery (12 columns): nade damage, flash efficiency, smoke timing
|
||||
- Economy Efficiency (8 columns): damage/$, eco/force/full round performance
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from typing import Dict, Any
|
||||
from .base_processor import BaseFeatureProcessor, SafeAggregator
|
||||
|
||||
|
||||
class TacticalProcessor(BaseFeatureProcessor):
|
||||
"""Tier 2 TACTICAL processor - Multi-table JOINs and conditional aggregations"""
|
||||
|
||||
MIN_MATCHES_REQUIRED = 5 # Need reasonable sample for tactical analysis
|
||||
|
||||
@staticmethod
|
||||
def calculate(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate all Tier 2 TACTICAL features (44 columns)
|
||||
|
||||
Returns dict with keys starting with 'tac_'
|
||||
"""
|
||||
features = {}
|
||||
|
||||
# Check minimum matches
|
||||
if not BaseFeatureProcessor.check_min_matches(steam_id, conn_l2,
|
||||
TacticalProcessor.MIN_MATCHES_REQUIRED):
|
||||
return _get_default_tactical_features()
|
||||
|
||||
# Calculate each tactical dimension
|
||||
features.update(TacticalProcessor._calculate_opening_impact(steam_id, conn_l2))
|
||||
features.update(TacticalProcessor._calculate_multikill(steam_id, conn_l2))
|
||||
features.update(TacticalProcessor._calculate_clutch(steam_id, conn_l2))
|
||||
features.update(TacticalProcessor._calculate_utility(steam_id, conn_l2))
|
||||
features.update(TacticalProcessor._calculate_economy(steam_id, conn_l2))
|
||||
|
||||
return features
|
||||
|
||||
@staticmethod
|
||||
def _calculate_opening_impact(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate Opening Impact (8 columns)
|
||||
|
||||
Columns:
|
||||
- tac_avg_fk, tac_avg_fd
|
||||
- tac_fk_rate, tac_fd_rate
|
||||
- tac_fk_success_rate (team win rate when player gets FK)
|
||||
- tac_entry_kill_rate, tac_entry_death_rate
|
||||
- tac_opening_duel_winrate
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
# FK/FD from fact_match_players
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
AVG(entry_kills) as avg_fk,
|
||||
AVG(entry_deaths) as avg_fd,
|
||||
SUM(entry_kills) as total_fk,
|
||||
SUM(entry_deaths) as total_fd,
|
||||
COUNT(*) as total_matches
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
avg_fk = row[0] if row[0] else 0.0
|
||||
avg_fd = row[1] if row[1] else 0.0
|
||||
total_fk = row[2] if row[2] else 0
|
||||
total_fd = row[3] if row[3] else 0
|
||||
total_matches = row[4] if row[4] else 1
|
||||
|
||||
opening_duels = total_fk + total_fd
|
||||
fk_rate = SafeAggregator.safe_divide(total_fk, opening_duels)
|
||||
fd_rate = SafeAggregator.safe_divide(total_fd, opening_duels)
|
||||
opening_duel_winrate = SafeAggregator.safe_divide(total_fk, opening_duels)
|
||||
|
||||
# FK success rate: team win rate when player gets FK
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as fk_matches,
|
||||
SUM(CASE WHEN is_win = 1 THEN 1 ELSE 0 END) as fk_wins
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
AND entry_kills > 0
|
||||
""", (steam_id,))
|
||||
|
||||
fk_row = cursor.fetchone()
|
||||
fk_matches = fk_row[0] if fk_row[0] else 0
|
||||
fk_wins = fk_row[1] if fk_row[1] else 0
|
||||
fk_success_rate = SafeAggregator.safe_divide(fk_wins, fk_matches)
|
||||
|
||||
# Entry kill/death rates (per T round for entry kills, total for entry deaths)
|
||||
cursor.execute("""
|
||||
SELECT COALESCE(SUM(round_total), 0)
|
||||
FROM fact_match_players_t
|
||||
WHERE steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
t_rounds = cursor.fetchone()[0] or 1
|
||||
|
||||
cursor.execute("""
|
||||
SELECT COALESCE(SUM(round_total), 0)
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
total_rounds = cursor.fetchone()[0] or 1
|
||||
|
||||
entry_kill_rate = SafeAggregator.safe_divide(total_fk, t_rounds)
|
||||
entry_death_rate = SafeAggregator.safe_divide(total_fd, total_rounds)
|
||||
|
||||
return {
|
||||
'tac_avg_fk': round(avg_fk, 2),
|
||||
'tac_avg_fd': round(avg_fd, 2),
|
||||
'tac_fk_rate': round(fk_rate, 3),
|
||||
'tac_fd_rate': round(fd_rate, 3),
|
||||
'tac_fk_success_rate': round(fk_success_rate, 3),
|
||||
'tac_entry_kill_rate': round(entry_kill_rate, 3),
|
||||
'tac_entry_death_rate': round(entry_death_rate, 3),
|
||||
'tac_opening_duel_winrate': round(opening_duel_winrate, 3),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_multikill(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate Multi-Kill Performance (6 columns)
|
||||
|
||||
Columns:
|
||||
- tac_avg_2k, tac_avg_3k, tac_avg_4k, tac_avg_5k
|
||||
- tac_multikill_rate
|
||||
- tac_ace_count
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
AVG(kill_2) as avg_2k,
|
||||
AVG(kill_3) as avg_3k,
|
||||
AVG(kill_4) as avg_4k,
|
||||
AVG(kill_5) as avg_5k,
|
||||
SUM(kill_2) as total_2k,
|
||||
SUM(kill_3) as total_3k,
|
||||
SUM(kill_4) as total_4k,
|
||||
SUM(kill_5) as total_5k,
|
||||
SUM(round_total) as total_rounds
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
avg_2k = row[0] if row[0] else 0.0
|
||||
avg_3k = row[1] if row[1] else 0.0
|
||||
avg_4k = row[2] if row[2] else 0.0
|
||||
avg_5k = row[3] if row[3] else 0.0
|
||||
total_2k = row[4] if row[4] else 0
|
||||
total_3k = row[5] if row[5] else 0
|
||||
total_4k = row[6] if row[6] else 0
|
||||
total_5k = row[7] if row[7] else 0
|
||||
total_rounds = row[8] if row[8] else 1
|
||||
|
||||
total_multikills = total_2k + total_3k + total_4k + total_5k
|
||||
multikill_rate = SafeAggregator.safe_divide(total_multikills, total_rounds)
|
||||
|
||||
return {
|
||||
'tac_avg_2k': round(avg_2k, 2),
|
||||
'tac_avg_3k': round(avg_3k, 2),
|
||||
'tac_avg_4k': round(avg_4k, 2),
|
||||
'tac_avg_5k': round(avg_5k, 2),
|
||||
'tac_multikill_rate': round(multikill_rate, 3),
|
||||
'tac_ace_count': total_5k,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_clutch(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate Clutch Performance (10 columns)
|
||||
|
||||
Columns:
|
||||
- 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
|
||||
|
||||
Logic:
|
||||
- Wins: Aggregated directly from fact_match_players (trusting upstream data).
|
||||
- Attempts: Calculated by replaying rounds with 'Active Player' filtering to remove ghosts.
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
# Step 1: Get Wins from fact_match_players
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(clutch_1v1) as c1,
|
||||
SUM(clutch_1v2) as c2,
|
||||
SUM(clutch_1v3) as c3,
|
||||
SUM(clutch_1v4) as c4,
|
||||
SUM(clutch_1v5) as c5
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
|
||||
wins_row = cursor.fetchone()
|
||||
clutch_1v1_wins = wins_row[0] if wins_row and wins_row[0] else 0
|
||||
clutch_1v2_wins = wins_row[1] if wins_row and wins_row[1] else 0
|
||||
clutch_1v3_wins = wins_row[2] if wins_row and wins_row[2] else 0
|
||||
clutch_1v4_wins = wins_row[3] if wins_row and wins_row[3] else 0
|
||||
clutch_1v5_wins = wins_row[4] if wins_row and wins_row[4] else 0
|
||||
|
||||
# Group 1v3+ wins
|
||||
clutch_1v3_plus_wins = clutch_1v3_wins + clutch_1v4_wins + clutch_1v5_wins
|
||||
|
||||
# Step 2: Calculate Attempts
|
||||
cursor.execute("SELECT DISTINCT match_id FROM fact_match_players WHERE steam_id_64 = ?", (steam_id,))
|
||||
match_ids = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
clutch_1v1_attempts = 0
|
||||
clutch_1v2_attempts = 0
|
||||
clutch_1v3_plus_attempts = 0
|
||||
|
||||
for match_id in match_ids:
|
||||
# Get Roster
|
||||
cursor.execute("SELECT steam_id_64, team_id FROM fact_match_players WHERE match_id = ?", (match_id,))
|
||||
roster = cursor.fetchall()
|
||||
|
||||
my_team_id = None
|
||||
for pid, tid in roster:
|
||||
if str(pid) == str(steam_id):
|
||||
my_team_id = tid
|
||||
break
|
||||
|
||||
if my_team_id is None:
|
||||
continue
|
||||
|
||||
all_teammates = {str(pid) for pid, tid in roster if tid == my_team_id}
|
||||
all_enemies = {str(pid) for pid, tid in roster if tid != my_team_id}
|
||||
|
||||
# Get Events for this match
|
||||
cursor.execute("""
|
||||
SELECT round_num, event_type, attacker_steam_id, victim_steam_id, event_time
|
||||
FROM fact_round_events
|
||||
WHERE match_id = ?
|
||||
ORDER BY round_num, event_time
|
||||
""", (match_id,))
|
||||
all_events = cursor.fetchall()
|
||||
|
||||
# Group events by round
|
||||
from collections import defaultdict
|
||||
events_by_round = defaultdict(list)
|
||||
active_players_by_round = defaultdict(set)
|
||||
|
||||
for r_num, e_type, attacker, victim, e_time in all_events:
|
||||
events_by_round[r_num].append((e_type, attacker, victim))
|
||||
if attacker: active_players_by_round[r_num].add(str(attacker))
|
||||
if victim: active_players_by_round[r_num].add(str(victim))
|
||||
|
||||
# Iterate rounds
|
||||
for r_num, round_events in events_by_round.items():
|
||||
active_players = active_players_by_round[r_num]
|
||||
|
||||
# If player not active, skip (probably camping or AFK or not spawned)
|
||||
if str(steam_id) not in active_players:
|
||||
continue
|
||||
|
||||
# Filter roster to active players only (removes ghosts)
|
||||
alive_teammates = all_teammates.intersection(active_players)
|
||||
alive_enemies = all_enemies.intersection(active_players)
|
||||
|
||||
# Safety: ensure player is in alive_teammates
|
||||
alive_teammates.add(str(steam_id))
|
||||
|
||||
clutch_detected = False
|
||||
|
||||
for e_type, attacker, victim in round_events:
|
||||
if e_type == 'kill':
|
||||
vic_str = str(victim)
|
||||
if vic_str in alive_teammates:
|
||||
alive_teammates.discard(vic_str)
|
||||
elif vic_str in alive_enemies:
|
||||
alive_enemies.discard(vic_str)
|
||||
|
||||
# Check clutch condition
|
||||
if not clutch_detected:
|
||||
# Teammates dead (len==1 means only me), Enemies alive
|
||||
if len(alive_teammates) == 1 and str(steam_id) in alive_teammates:
|
||||
enemies_cnt = len(alive_enemies)
|
||||
if enemies_cnt > 0:
|
||||
clutch_detected = True
|
||||
if enemies_cnt == 1:
|
||||
clutch_1v1_attempts += 1
|
||||
elif enemies_cnt == 2:
|
||||
clutch_1v2_attempts += 1
|
||||
elif enemies_cnt >= 3:
|
||||
clutch_1v3_plus_attempts += 1
|
||||
|
||||
# Calculate win rates
|
||||
rate_1v1 = SafeAggregator.safe_divide(clutch_1v1_wins, clutch_1v1_attempts)
|
||||
rate_1v2 = SafeAggregator.safe_divide(clutch_1v2_wins, clutch_1v2_attempts)
|
||||
rate_1v3_plus = SafeAggregator.safe_divide(clutch_1v3_plus_wins, clutch_1v3_plus_attempts)
|
||||
|
||||
# Clutch impact score: weighted by difficulty
|
||||
impact_score = (clutch_1v1_wins * 1.0 + clutch_1v2_wins * 3.0 + clutch_1v3_plus_wins * 7.0)
|
||||
|
||||
return {
|
||||
'tac_clutch_1v1_attempts': clutch_1v1_attempts,
|
||||
'tac_clutch_1v1_wins': clutch_1v1_wins,
|
||||
'tac_clutch_1v1_rate': round(rate_1v1, 3),
|
||||
'tac_clutch_1v2_attempts': clutch_1v2_attempts,
|
||||
'tac_clutch_1v2_wins': clutch_1v2_wins,
|
||||
'tac_clutch_1v2_rate': round(rate_1v2, 3),
|
||||
'tac_clutch_1v3_plus_attempts': clutch_1v3_plus_attempts,
|
||||
'tac_clutch_1v3_plus_wins': clutch_1v3_plus_wins,
|
||||
'tac_clutch_1v3_plus_rate': round(rate_1v3_plus, 3),
|
||||
'tac_clutch_impact_score': round(impact_score, 2)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_utility(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate Utility Mastery (12 columns)
|
||||
|
||||
Columns:
|
||||
- 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
|
||||
|
||||
Note: Requires fact_round_player_economy for detailed utility stats
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
# Check if economy table exists (leetify mode)
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) FROM sqlite_master
|
||||
WHERE type='table' AND name='fact_round_player_economy'
|
||||
""")
|
||||
|
||||
has_economy = cursor.fetchone()[0] > 0
|
||||
|
||||
if not has_economy:
|
||||
# Return zeros if no economy data
|
||||
return {
|
||||
'tac_util_flash_per_round': 0.0,
|
||||
'tac_util_smoke_per_round': 0.0,
|
||||
'tac_util_molotov_per_round': 0.0,
|
||||
'tac_util_he_per_round': 0.0,
|
||||
'tac_util_usage_rate': 0.0,
|
||||
'tac_util_nade_dmg_per_round': 0.0,
|
||||
'tac_util_nade_dmg_per_nade': 0.0,
|
||||
'tac_util_flash_time_per_round': 0.0,
|
||||
'tac_util_flash_enemies_per_round': 0.0,
|
||||
'tac_util_flash_efficiency': 0.0,
|
||||
'tac_util_smoke_timing_score': 0.0,
|
||||
'tac_util_impact_score': 0.0,
|
||||
}
|
||||
|
||||
# Get total rounds for per-round calculations
|
||||
total_rounds = BaseFeatureProcessor.get_player_round_count(steam_id, conn_l2)
|
||||
if total_rounds == 0:
|
||||
total_rounds = 1
|
||||
|
||||
# Utility usage from fact_match_players
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(util_flash_usage) as total_flash,
|
||||
SUM(util_smoke_usage) as total_smoke,
|
||||
SUM(util_molotov_usage) as total_molotov,
|
||||
SUM(util_he_usage) as total_he,
|
||||
SUM(flash_enemy) as enemies_flashed,
|
||||
SUM(damage_total) as total_damage,
|
||||
SUM(throw_harm_enemy) as nade_damage,
|
||||
COUNT(*) as matches
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
total_flash = row[0] if row[0] else 0
|
||||
total_smoke = row[1] if row[1] else 0
|
||||
total_molotov = row[2] if row[2] else 0
|
||||
total_he = row[3] if row[3] else 0
|
||||
enemies_flashed = row[4] if row[4] else 0
|
||||
total_damage = row[5] if row[5] else 0
|
||||
nade_damage = row[6] if row[6] else 0
|
||||
rounds_with_data = row[7] if row[7] else 1
|
||||
|
||||
total_nades = total_flash + total_smoke + total_molotov + total_he
|
||||
|
||||
flash_per_round = total_flash / total_rounds
|
||||
smoke_per_round = total_smoke / total_rounds
|
||||
molotov_per_round = total_molotov / total_rounds
|
||||
he_per_round = total_he / total_rounds
|
||||
usage_rate = total_nades / total_rounds
|
||||
|
||||
# Nade damage (HE grenade + molotov damage from throw_harm_enemy)
|
||||
nade_dmg_per_round = SafeAggregator.safe_divide(nade_damage, total_rounds)
|
||||
nade_dmg_per_nade = SafeAggregator.safe_divide(nade_damage, total_he + total_molotov)
|
||||
|
||||
# Flash efficiency (simplified - kills per flash from match data)
|
||||
# DEPRECATED: Replaced by Enemies Blinded per Flash logic below
|
||||
# cursor.execute("""
|
||||
# SELECT SUM(kills) as total_kills
|
||||
# FROM fact_match_players
|
||||
# WHERE steam_id_64 = ?
|
||||
# """, (steam_id,))
|
||||
#
|
||||
# total_kills = cursor.fetchone()[0]
|
||||
# total_kills = total_kills if total_kills else 0
|
||||
# flash_efficiency = SafeAggregator.safe_divide(total_kills, total_flash)
|
||||
|
||||
# Real flash data from fact_match_players
|
||||
# flash_time in L2 is TOTAL flash time (seconds), not average
|
||||
# flash_enemy is TOTAL enemies flashed
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(flash_time) as total_flash_time,
|
||||
SUM(flash_enemy) as total_enemies_flashed,
|
||||
SUM(util_flash_usage) as total_flashes_thrown
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
flash_row = cursor.fetchone()
|
||||
total_flash_time = flash_row[0] if flash_row and flash_row[0] else 0.0
|
||||
total_enemies_flashed = flash_row[1] if flash_row and flash_row[1] else 0
|
||||
total_flashes_thrown = flash_row[2] if flash_row and flash_row[2] else 0
|
||||
|
||||
flash_time_per_round = total_flash_time / total_rounds if total_rounds > 0 else 0.0
|
||||
flash_enemies_per_round = total_enemies_flashed / total_rounds if total_rounds > 0 else 0.0
|
||||
|
||||
# Flash Efficiency: Enemies Blinded per Flash Thrown (instead of kills per flash)
|
||||
# 100% means 1 enemy blinded per flash
|
||||
# 200% means 2 enemies blinded per flash (very good)
|
||||
flash_efficiency = SafeAggregator.safe_divide(total_enemies_flashed, total_flashes_thrown)
|
||||
|
||||
# Smoke timing score CANNOT be calculated without bomb plant event timestamps
|
||||
# Would require: SELECT event_time FROM fact_round_events WHERE event_type = 'bomb_plant'
|
||||
# Then correlate with util_smoke_usage timing - currently no timing data for utility usage
|
||||
# Commenting out: tac_util_smoke_timing_score
|
||||
smoke_timing_score = 0.0
|
||||
|
||||
# Taser Kills Logic (Zeus)
|
||||
# We want Attempts (shots fired) vs Kills
|
||||
# User requested to track "Equipped Count" instead of "Attempts" (shots)
|
||||
# because event logs often miss weapon_fire for taser.
|
||||
|
||||
# We check fact_round_player_economy for has_zeus = 1
|
||||
zeus_equipped_count = 0
|
||||
if has_economy:
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM fact_round_player_economy
|
||||
WHERE steam_id_64 = ? AND has_zeus = 1
|
||||
""", (steam_id,))
|
||||
zeus_equipped_count = cursor.fetchone()[0] or 0
|
||||
|
||||
# Kills still come from event logs
|
||||
# Removed tac_util_zeus_kills per user request (data not available)
|
||||
# cursor.execute("""
|
||||
# SELECT
|
||||
# COUNT(CASE WHEN event_type = 'kill' AND weapon = 'taser' THEN 1 END) as kills
|
||||
# FROM fact_round_events
|
||||
# WHERE attacker_steam_id = ?
|
||||
# """, (steam_id,))
|
||||
# zeus_kills = cursor.fetchone()[0] or 0
|
||||
|
||||
# Fallback: if equipped count < kills (shouldn't happen if economy data is good), fix it
|
||||
# if zeus_equipped_count < zeus_kills:
|
||||
# zeus_equipped_count = zeus_kills
|
||||
|
||||
# Utility impact score (composite)
|
||||
impact_score = (
|
||||
nade_dmg_per_round * 0.3 +
|
||||
flash_efficiency * 2.0 +
|
||||
usage_rate * 10.0
|
||||
)
|
||||
|
||||
return {
|
||||
'tac_util_flash_per_round': round(flash_per_round, 2),
|
||||
'tac_util_smoke_per_round': round(smoke_per_round, 2),
|
||||
'tac_util_molotov_per_round': round(molotov_per_round, 2),
|
||||
'tac_util_he_per_round': round(he_per_round, 2),
|
||||
'tac_util_usage_rate': round(usage_rate, 2),
|
||||
'tac_util_nade_dmg_per_round': round(nade_dmg_per_round, 2),
|
||||
'tac_util_nade_dmg_per_nade': round(nade_dmg_per_nade, 2),
|
||||
'tac_util_flash_time_per_round': round(flash_time_per_round, 2),
|
||||
'tac_util_flash_enemies_per_round': round(flash_enemies_per_round, 2),
|
||||
'tac_util_flash_efficiency': round(flash_efficiency, 3),
|
||||
#'tac_util_smoke_timing_score': round(smoke_timing_score, 2), # Removed per user request
|
||||
'tac_util_impact_score': round(impact_score, 2),
|
||||
'tac_util_zeus_equipped_count': zeus_equipped_count,
|
||||
#'tac_util_zeus_kills': zeus_kills, # Removed
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_economy(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate Economy Efficiency (8 columns)
|
||||
|
||||
Columns:
|
||||
- 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
|
||||
|
||||
Note: Requires fact_round_player_economy for equipment values
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
# Check if economy table exists
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) FROM sqlite_master
|
||||
WHERE type='table' AND name='fact_round_player_economy'
|
||||
""")
|
||||
|
||||
has_economy = cursor.fetchone()[0] > 0
|
||||
|
||||
if not has_economy:
|
||||
# Return zeros if no economy data
|
||||
return {
|
||||
'tac_eco_dmg_per_1k': 0.0,
|
||||
'tac_eco_kpr_eco_rounds': 0.0,
|
||||
'tac_eco_kd_eco_rounds': 0.0,
|
||||
'tac_eco_kpr_force_rounds': 0.0,
|
||||
'tac_eco_kpr_full_rounds': 0.0,
|
||||
'tac_eco_save_discipline': 0.0,
|
||||
'tac_eco_force_success_rate': 0.0,
|
||||
'tac_eco_efficiency_score': 0.0,
|
||||
}
|
||||
|
||||
# REAL economy-based performance from round-level data
|
||||
# Join fact_round_player_economy with fact_round_events to get kills/deaths per economy state
|
||||
|
||||
# Fallback if no economy table but we want basic DMG/1k approximation from total damage / assumed average buy
|
||||
# But avg_equip_value is from economy table.
|
||||
# If no economy table, we can't do this accurately.
|
||||
|
||||
# However, user says "Eco Dmg/1k" is 0.00.
|
||||
# If we have NO economy table, we returned early above.
|
||||
# If we reached here, we HAVE economy table (or at least check passed).
|
||||
# Let's check logic.
|
||||
|
||||
# Get average equipment value
|
||||
cursor.execute("""
|
||||
SELECT AVG(equipment_value)
|
||||
FROM fact_round_player_economy
|
||||
WHERE steam_id_64 = ?
|
||||
AND equipment_value IS NOT NULL
|
||||
AND equipment_value > 0 -- Filter out zero equipment value rounds? Or include them?
|
||||
""", (steam_id,))
|
||||
avg_equip_val_res = cursor.fetchone()
|
||||
avg_equip_value = avg_equip_val_res[0] if avg_equip_val_res and avg_equip_val_res[0] else 4000.0
|
||||
|
||||
# Avoid division by zero if avg_equip_value is somehow 0
|
||||
if avg_equip_value < 100: avg_equip_value = 4000.0
|
||||
|
||||
# Get total damage and calculate dmg per $1000
|
||||
cursor.execute("""
|
||||
SELECT SUM(damage_total), SUM(round_total)
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
damage_row = cursor.fetchone()
|
||||
total_damage = damage_row[0] if damage_row[0] else 0
|
||||
total_rounds = damage_row[1] if damage_row[1] else 1
|
||||
|
||||
avg_dmg_per_round = SafeAggregator.safe_divide(total_damage, total_rounds)
|
||||
|
||||
# Formula: (ADR) / (AvgSpend / 1000)
|
||||
# e.g. 80 ADR / (4000 / 1000) = 80 / 4 = 20 dmg/$1k
|
||||
dmg_per_1k = SafeAggregator.safe_divide(avg_dmg_per_round, (avg_equip_value / 1000.0))
|
||||
|
||||
# ECO rounds: equipment_value < 2000
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
e.match_id,
|
||||
e.round_num,
|
||||
e.steam_id_64,
|
||||
COUNT(CASE WHEN fre.event_type = 'kill' AND fre.attacker_steam_id = e.steam_id_64 THEN 1 END) as kills,
|
||||
COUNT(CASE WHEN fre.event_type = 'kill' AND fre.victim_steam_id = e.steam_id_64 THEN 1 END) as deaths
|
||||
FROM fact_round_player_economy e
|
||||
LEFT JOIN fact_round_events fre ON e.match_id = fre.match_id AND e.round_num = fre.round_num
|
||||
WHERE e.steam_id_64 = ?
|
||||
AND e.equipment_value < 2000
|
||||
GROUP BY e.match_id, e.round_num, e.steam_id_64
|
||||
""", (steam_id,))
|
||||
|
||||
eco_rounds = cursor.fetchall()
|
||||
eco_kills = sum(row[3] for row in eco_rounds)
|
||||
eco_deaths = sum(row[4] for row in eco_rounds)
|
||||
eco_round_count = len(eco_rounds)
|
||||
|
||||
kpr_eco = SafeAggregator.safe_divide(eco_kills, eco_round_count)
|
||||
kd_eco = SafeAggregator.safe_divide(eco_kills, eco_deaths)
|
||||
|
||||
# FORCE rounds: 2000 <= equipment_value < 3500
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
e.match_id,
|
||||
e.round_num,
|
||||
e.steam_id_64,
|
||||
COUNT(CASE WHEN fre.event_type = 'kill' AND fre.attacker_steam_id = e.steam_id_64 THEN 1 END) as kills,
|
||||
fr.winner_side,
|
||||
e.side
|
||||
FROM fact_round_player_economy e
|
||||
LEFT JOIN fact_round_events fre ON e.match_id = fre.match_id AND e.round_num = fre.round_num
|
||||
LEFT JOIN fact_rounds fr ON e.match_id = fr.match_id AND e.round_num = fr.round_num
|
||||
WHERE e.steam_id_64 = ?
|
||||
AND e.equipment_value >= 2000
|
||||
AND e.equipment_value < 3500
|
||||
GROUP BY e.match_id, e.round_num, e.steam_id_64, fr.winner_side, e.side
|
||||
""", (steam_id,))
|
||||
|
||||
force_rounds = cursor.fetchall()
|
||||
force_kills = sum(row[3] for row in force_rounds)
|
||||
force_round_count = len(force_rounds)
|
||||
force_wins = sum(1 for row in force_rounds if row[4] == row[5]) # winner_side == player_side
|
||||
|
||||
kpr_force = SafeAggregator.safe_divide(force_kills, force_round_count)
|
||||
force_success = SafeAggregator.safe_divide(force_wins, force_round_count)
|
||||
|
||||
# FULL BUY rounds: equipment_value >= 3500
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
e.match_id,
|
||||
e.round_num,
|
||||
e.steam_id_64,
|
||||
COUNT(CASE WHEN fre.event_type = 'kill' AND fre.attacker_steam_id = e.steam_id_64 THEN 1 END) as kills
|
||||
FROM fact_round_player_economy e
|
||||
LEFT JOIN fact_round_events fre ON e.match_id = fre.match_id AND e.round_num = fre.round_num
|
||||
WHERE e.steam_id_64 = ?
|
||||
AND e.equipment_value >= 3500
|
||||
GROUP BY e.match_id, e.round_num, e.steam_id_64
|
||||
""", (steam_id,))
|
||||
|
||||
full_rounds = cursor.fetchall()
|
||||
full_kills = sum(row[3] for row in full_rounds)
|
||||
full_round_count = len(full_rounds)
|
||||
|
||||
kpr_full = SafeAggregator.safe_divide(full_kills, full_round_count)
|
||||
|
||||
# Save discipline: ratio of eco rounds to total rounds (lower is better discipline)
|
||||
save_discipline = 1.0 - SafeAggregator.safe_divide(eco_round_count, total_rounds)
|
||||
|
||||
# Efficiency score: weighted KPR across economy states
|
||||
efficiency_score = (kpr_eco * 1.5 + kpr_force * 1.2 + kpr_full * 1.0) / 3.7
|
||||
|
||||
return {
|
||||
'tac_eco_dmg_per_1k': round(dmg_per_1k, 2),
|
||||
'tac_eco_kpr_eco_rounds': round(kpr_eco, 3),
|
||||
'tac_eco_kd_eco_rounds': round(kd_eco, 3),
|
||||
'tac_eco_kpr_force_rounds': round(kpr_force, 3),
|
||||
'tac_eco_kpr_full_rounds': round(kpr_full, 3),
|
||||
'tac_eco_save_discipline': round(save_discipline, 3),
|
||||
'tac_eco_force_success_rate': round(force_success, 3),
|
||||
'tac_eco_efficiency_score': round(efficiency_score, 2),
|
||||
}
|
||||
|
||||
|
||||
def _get_default_tactical_features() -> Dict[str, Any]:
|
||||
"""Return default zero values for all 44 TACTICAL features"""
|
||||
return {
|
||||
# Opening Impact (8)
|
||||
'tac_avg_fk': 0.0,
|
||||
'tac_avg_fd': 0.0,
|
||||
'tac_fk_rate': 0.0,
|
||||
'tac_fd_rate': 0.0,
|
||||
'tac_fk_success_rate': 0.0,
|
||||
'tac_entry_kill_rate': 0.0,
|
||||
'tac_entry_death_rate': 0.0,
|
||||
'tac_opening_duel_winrate': 0.0,
|
||||
# Multi-Kill (6)
|
||||
'tac_avg_2k': 0.0,
|
||||
'tac_avg_3k': 0.0,
|
||||
'tac_avg_4k': 0.0,
|
||||
'tac_avg_5k': 0.0,
|
||||
'tac_multikill_rate': 0.0,
|
||||
'tac_ace_count': 0,
|
||||
# Clutch Performance (10)
|
||||
'tac_clutch_1v1_attempts': 0,
|
||||
'tac_clutch_1v1_wins': 0,
|
||||
'tac_clutch_1v1_rate': 0.0,
|
||||
'tac_clutch_1v2_attempts': 0,
|
||||
'tac_clutch_1v2_wins': 0,
|
||||
'tac_clutch_1v2_rate': 0.0,
|
||||
'tac_clutch_1v3_plus_attempts': 0,
|
||||
'tac_clutch_1v3_plus_wins': 0,
|
||||
'tac_clutch_1v3_plus_rate': 0.0,
|
||||
'tac_clutch_impact_score': 0.0,
|
||||
# Utility Mastery (12)
|
||||
'tac_util_flash_per_round': 0.0,
|
||||
'tac_util_smoke_per_round': 0.0,
|
||||
'tac_util_molotov_per_round': 0.0,
|
||||
'tac_util_he_per_round': 0.0,
|
||||
'tac_util_usage_rate': 0.0,
|
||||
'tac_util_nade_dmg_per_round': 0.0,
|
||||
'tac_util_nade_dmg_per_nade': 0.0,
|
||||
'tac_util_flash_time_per_round': 0.0,
|
||||
'tac_util_flash_enemies_per_round': 0.0,
|
||||
'tac_util_flash_efficiency': 0.0,
|
||||
# 'tac_util_smoke_timing_score': 0.0, # Removed
|
||||
'tac_util_impact_score': 0.0,
|
||||
'tac_util_zeus_equipped_count': 0,
|
||||
# 'tac_util_zeus_kills': 0, # Removed
|
||||
# Economy Efficiency (8)
|
||||
'tac_eco_dmg_per_1k': 0.0,
|
||||
'tac_eco_kpr_eco_rounds': 0.0,
|
||||
'tac_eco_kd_eco_rounds': 0.0,
|
||||
'tac_eco_kpr_force_rounds': 0.0,
|
||||
'tac_eco_kpr_full_rounds': 0.0,
|
||||
'tac_eco_save_discipline': 0.0,
|
||||
'tac_eco_force_success_rate': 0.0,
|
||||
'tac_eco_efficiency_score': 0.0,
|
||||
}
|
||||
Reference in New Issue
Block a user