Files
yrtv/database/L3/processors/basic_processor.py

464 lines
17 KiB
Python

"""
BasicProcessor - Tier 1: CORE Features (41 columns)
Calculates fundamental player statistics from fact_match_players:
- Basic Performance (15 columns): rating, kd, adr, kast, rws, hs%, kills, deaths, assists
- Match Stats (8 columns): win_rate, mvps, duration, elo
- Weapon Stats (12 columns): awp, knife, zeus, diversity
- Objective Stats (6 columns): plants, defuses, flash_assists
"""
import sqlite3
from typing import Dict, Any
from .base_processor import BaseFeatureProcessor, SafeAggregator, WeaponCategories
class BasicProcessor(BaseFeatureProcessor):
"""Tier 1 CORE processor - Direct aggregations from fact_match_players"""
MIN_MATCHES_REQUIRED = 1 # Basic stats work with any match count
@staticmethod
def calculate(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate all Tier 1 CORE features (41 columns)
Returns dict with keys:
- core_avg_rating, core_avg_rating2, core_avg_kd, core_avg_adr, etc.
"""
features = {}
# Get match count first
match_count = BaseFeatureProcessor.get_player_match_count(steam_id, conn_l2)
if match_count == 0:
return _get_default_features()
# Calculate each sub-section
features.update(BasicProcessor._calculate_basic_performance(steam_id, conn_l2))
features.update(BasicProcessor._calculate_match_stats(steam_id, conn_l2))
features.update(BasicProcessor._calculate_weapon_stats(steam_id, conn_l2))
features.update(BasicProcessor._calculate_objective_stats(steam_id, conn_l2))
return features
@staticmethod
def _calculate_basic_performance(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Basic Performance (15 columns)
Columns:
- 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
"""
cursor = conn_l2.cursor()
# Main aggregation query
cursor.execute("""
SELECT
AVG(rating) as avg_rating,
AVG(rating2) as avg_rating2,
AVG(CAST(kills AS REAL) / NULLIF(deaths, 0)) as avg_kd,
AVG(adr) as avg_adr,
AVG(kast) as avg_kast,
AVG(rws) as avg_rws,
AVG(headshot_count) as avg_hs_kills,
SUM(kills) as total_kills,
SUM(deaths) as total_deaths,
SUM(headshot_count) as total_hs,
SUM(assists) as total_assists,
AVG(assists) as avg_assists,
SUM(round_total) as total_rounds
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
row = cursor.fetchone()
if not row:
return {}
total_kills = row[7] if row[7] else 0
total_deaths = row[8] if row[8] else 1
total_hs = row[9] if row[9] else 0
total_rounds = row[12] if row[12] else 1
return {
'core_avg_rating': round(row[0], 3) if row[0] else 0.0,
'core_avg_rating2': round(row[1], 3) if row[1] else 0.0,
'core_avg_kd': round(row[2], 3) if row[2] else 0.0,
'core_avg_adr': round(row[3], 2) if row[3] else 0.0,
'core_avg_kast': round(row[4], 3) if row[4] else 0.0,
'core_avg_rws': round(row[5], 2) if row[5] else 0.0,
'core_avg_hs_kills': round(row[6], 2) if row[6] else 0.0,
'core_hs_rate': round(total_hs / total_kills, 3) if total_kills > 0 else 0.0,
'core_total_kills': total_kills,
'core_total_deaths': total_deaths,
'core_total_assists': row[10] if row[10] else 0,
'core_avg_assists': round(row[11], 2) if row[11] else 0.0,
'core_kpr': round(total_kills / total_rounds, 3) if total_rounds > 0 else 0.0,
'core_dpr': round(total_deaths / total_rounds, 3) if total_rounds > 0 else 0.0,
'core_survival_rate': round((total_rounds - total_deaths) / total_rounds, 3) if total_rounds > 0 else 0.0,
}
@staticmethod
def _calculate_flash_assists(steam_id: str, conn_l2: sqlite3.Connection) -> int:
"""
Calculate flash assists from fact_match_players (Total - Damage Assists)
Returns total flash assist count (Estimated)
"""
cursor = conn_l2.cursor()
# NOTE: Flash Assist Logic
# Source 'flash_assists' is often 0.
# User Logic: Flash Assists = Total Assists - Damage Assists (assisted_kill)
# We take MAX(0, diff) to avoid negative numbers if assisted_kill definition varies.
cursor.execute("""
SELECT SUM(MAX(0, assists - assisted_kill))
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
res = cursor.fetchone()
if res and res[0] is not None:
return res[0]
return 0
@staticmethod
def _calculate_match_stats(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Match Stats (8 columns)
Columns:
- 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
"""
cursor = conn_l2.cursor()
# Win/loss stats
cursor.execute("""
SELECT
COUNT(*) as total_matches,
SUM(CASE WHEN is_win = 1 THEN 1 ELSE 0 END) as wins,
SUM(CASE WHEN is_win = 0 THEN 1 ELSE 0 END) as losses,
AVG(mvp_count) as avg_mvps,
SUM(mvp_count) as total_mvps
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
row = cursor.fetchone()
total_matches = row[0] if row[0] else 0
wins = row[1] if row[1] else 0
losses = row[2] if row[2] else 0
avg_mvps = row[3] if row[3] else 0.0
total_mvps = row[4] if row[4] else 0
# Match duration (from fact_matches)
cursor.execute("""
SELECT AVG(m.duration) as avg_duration
FROM fact_matches m
JOIN fact_match_players p ON m.match_id = p.match_id
WHERE p.steam_id_64 = ?
""", (steam_id,))
duration_row = cursor.fetchone()
avg_duration = duration_row[0] if duration_row and duration_row[0] else 0
# ELO stats (from elo_change column)
cursor.execute("""
SELECT
AVG(elo_change) as avg_elo_change,
SUM(elo_change) as total_elo_gained
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
elo_row = cursor.fetchone()
avg_elo_change = elo_row[0] if elo_row and elo_row[0] else 0.0
total_elo_gained = elo_row[1] if elo_row and elo_row[1] else 0.0
return {
'core_win_rate': round(wins / total_matches, 3) if total_matches > 0 else 0.0,
'core_wins': wins,
'core_losses': losses,
'core_avg_match_duration': int(avg_duration),
'core_avg_mvps': round(avg_mvps, 2),
'core_mvp_rate': round(total_mvps / total_matches, 2) if total_matches > 0 else 0.0,
'core_avg_elo_change': round(avg_elo_change, 2),
'core_total_elo_gained': round(total_elo_gained, 2),
}
@staticmethod
def _calculate_weapon_stats(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Weapon Stats (12 columns)
Columns:
- core_avg_awp_kills, core_awp_usage_rate
- core_avg_knife_kills, core_avg_zeus_kills, core_zeus_buy_rate
- core_top_weapon, core_top_weapon_kills, core_top_weapon_hs_rate
- core_weapon_diversity
- core_rifle_hs_rate, core_pistol_hs_rate
- core_smg_kills_total
"""
cursor = conn_l2.cursor()
# AWP/Knife/Zeus stats from fact_round_events
cursor.execute("""
SELECT
weapon,
COUNT(*) as kill_count
FROM fact_round_events
WHERE attacker_steam_id = ?
AND weapon IN ('AWP', 'Knife', 'Zeus', 'knife', 'awp', 'zeus')
GROUP BY weapon
""", (steam_id,))
awp_kills = 0
knife_kills = 0
zeus_kills = 0
for weapon, kills in cursor.fetchall():
weapon_lower = weapon.lower() if weapon else ''
if weapon_lower == 'awp':
awp_kills += kills
elif weapon_lower == 'knife':
knife_kills += kills
elif weapon_lower == 'zeus':
zeus_kills += kills
# Get total matches count for rates
cursor.execute("""
SELECT COUNT(DISTINCT match_id)
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
total_matches = cursor.fetchone()[0] or 1
avg_awp = awp_kills / total_matches
avg_knife = knife_kills / total_matches
avg_zeus = zeus_kills / total_matches
# Flash assists from fact_round_events
flash_assists = BasicProcessor._calculate_flash_assists(steam_id, conn_l2)
avg_flash_assists = flash_assists / total_matches
# Top weapon from fact_round_events
cursor.execute("""
SELECT
weapon,
COUNT(*) as kill_count,
SUM(CASE WHEN is_headshot = 1 THEN 1 ELSE 0 END) as hs_count
FROM fact_round_events
WHERE attacker_steam_id = ?
AND weapon IS NOT NULL
AND weapon != 'unknown'
GROUP BY weapon
ORDER BY kill_count DESC
LIMIT 1
""", (steam_id,))
weapon_row = cursor.fetchone()
top_weapon = weapon_row[0] if weapon_row else "unknown"
top_weapon_kills = weapon_row[1] if weapon_row else 0
top_weapon_hs = weapon_row[2] if weapon_row else 0
top_weapon_hs_rate = top_weapon_hs / top_weapon_kills if top_weapon_kills > 0 else 0.0
# Weapon diversity (number of distinct weapons with 10+ kills)
cursor.execute("""
SELECT COUNT(DISTINCT weapon) as weapon_count
FROM (
SELECT weapon, COUNT(*) as kills
FROM fact_round_events
WHERE attacker_steam_id = ?
AND weapon IS NOT NULL
GROUP BY weapon
HAVING kills >= 10
)
""", (steam_id,))
diversity_row = cursor.fetchone()
weapon_diversity = diversity_row[0] if diversity_row else 0
# Rifle/Pistol/SMG stats
cursor.execute("""
SELECT
weapon,
COUNT(*) as kills,
SUM(CASE WHEN is_headshot = 1 THEN 1 ELSE 0 END) as headshot_kills
FROM fact_round_events
WHERE attacker_steam_id = ?
AND weapon IS NOT NULL
GROUP BY weapon
""", (steam_id,))
rifle_kills = 0
rifle_hs = 0
pistol_kills = 0
pistol_hs = 0
smg_kills = 0
awp_usage_count = 0
for weapon, kills, hs in cursor.fetchall():
category = WeaponCategories.get_category(weapon)
if category == 'rifle':
rifle_kills += kills
rifle_hs += hs
elif category == 'pistol':
pistol_kills += kills
pistol_hs += hs
elif category == 'smg':
smg_kills += kills
elif weapon.lower() == 'awp':
awp_usage_count += kills
total_rounds = BaseFeatureProcessor.get_player_round_count(steam_id, conn_l2)
return {
'core_avg_awp_kills': round(avg_awp, 2),
'core_awp_usage_rate': round(awp_usage_count / total_rounds, 3) if total_rounds > 0 else 0.0,
'core_avg_knife_kills': round(avg_knife, 3),
'core_avg_zeus_kills': round(avg_zeus, 3),
'core_zeus_buy_rate': round(avg_zeus / total_matches, 3) if total_matches > 0 else 0.0,
'core_avg_flash_assists': round(avg_flash_assists, 2),
'core_top_weapon': top_weapon,
'core_top_weapon_kills': top_weapon_kills,
'core_top_weapon_hs_rate': round(top_weapon_hs_rate, 3),
'core_weapon_diversity': weapon_diversity,
'core_rifle_hs_rate': round(rifle_hs / rifle_kills, 3) if rifle_kills > 0 else 0.0,
'core_pistol_hs_rate': round(pistol_hs / pistol_kills, 3) if pistol_kills > 0 else 0.0,
'core_smg_kills_total': smg_kills,
}
@staticmethod
def _calculate_objective_stats(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
"""
Calculate Objective Stats (6 columns)
Columns:
- core_avg_plants, core_avg_defuses, core_avg_flash_assists
- core_plant_success_rate, core_defuse_success_rate
- core_objective_impact
"""
cursor = conn_l2.cursor()
# Get data from main table
# Updated to use calculated flash assists formula
# Calculate flash assists manually first (since column is 0)
flash_assists_total = BasicProcessor._calculate_flash_assists(steam_id, conn_l2)
match_count = BaseFeatureProcessor.get_player_match_count(steam_id, conn_l2)
avg_flash_assists = flash_assists_total / match_count if match_count > 0 else 0.0
cursor.execute("""
SELECT
AVG(planted_bomb) as avg_plants,
AVG(defused_bomb) as avg_defuses,
SUM(planted_bomb) as total_plants,
SUM(defused_bomb) as total_defuses
FROM fact_match_players
WHERE steam_id_64 = ?
""", (steam_id,))
row = cursor.fetchone()
if not row:
return {}
avg_plants = row[0] if row[0] else 0.0
avg_defuses = row[1] if row[1] else 0.0
# avg_flash_assists computed above
total_plants = row[2] if row[2] else 0
total_defuses = row[3] if row[3] else 0
# Get T side rounds
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
# Get CT side rounds
cursor.execute("""
SELECT COALESCE(SUM(round_total), 0)
FROM fact_match_players_ct
WHERE steam_id_64 = ?
""", (steam_id,))
ct_rounds = cursor.fetchone()[0] or 1
# Plant success rate: plants per T round
plant_rate = total_plants / t_rounds if t_rounds > 0 else 0.0
# Defuse success rate: approximate as defuses per CT round (simplified)
defuse_rate = total_defuses / ct_rounds if ct_rounds > 0 else 0.0
# Objective impact score: weighted combination
objective_impact = (total_plants * 2.0 + total_defuses * 3.0 + avg_flash_assists * 0.5)
return {
'core_avg_plants': round(avg_plants, 2),
'core_avg_defuses': round(avg_defuses, 2),
'core_avg_flash_assists': round(avg_flash_assists, 2),
'core_plant_success_rate': round(plant_rate, 3),
'core_defuse_success_rate': round(defuse_rate, 3),
'core_objective_impact': round(objective_impact, 2),
}
def _get_default_features() -> Dict[str, Any]:
"""Return default zero values for all 41 CORE features"""
return {
# Basic Performance (15)
'core_avg_rating': 0.0,
'core_avg_rating2': 0.0,
'core_avg_kd': 0.0,
'core_avg_adr': 0.0,
'core_avg_kast': 0.0,
'core_avg_rws': 0.0,
'core_avg_hs_kills': 0.0,
'core_hs_rate': 0.0,
'core_total_kills': 0,
'core_total_deaths': 0,
'core_total_assists': 0,
'core_avg_assists': 0.0,
'core_kpr': 0.0,
'core_dpr': 0.0,
'core_survival_rate': 0.0,
# Match Stats (8)
'core_win_rate': 0.0,
'core_wins': 0,
'core_losses': 0,
'core_avg_match_duration': 0,
'core_avg_mvps': 0.0,
'core_mvp_rate': 0.0,
'core_avg_elo_change': 0.0,
'core_total_elo_gained': 0.0,
# Weapon Stats (12)
'core_avg_awp_kills': 0.0,
'core_awp_usage_rate': 0.0,
'core_avg_knife_kills': 0.0,
'core_avg_zeus_kills': 0.0,
'core_zeus_buy_rate': 0.0,
'core_top_weapon': 'unknown',
'core_top_weapon_kills': 0,
'core_top_weapon_hs_rate': 0.0,
'core_weapon_diversity': 0,
'core_rifle_hs_rate': 0.0,
'core_pistol_hs_rate': 0.0,
'core_smg_kills_total': 0,
# Objective Stats (6)
'core_avg_plants': 0.0,
'core_avg_defuses': 0.0,
'core_avg_flash_assists': 0.0,
'core_plant_success_rate': 0.0,
'core_defuse_success_rate': 0.0,
'core_objective_impact': 0.0,
}