3.0.0 : Reconstructed Database System.
This commit is contained in:
720
database/L3/processors/meta_processor.py
Normal file
720
database/L3/processors/meta_processor.py
Normal file
@@ -0,0 +1,720 @@
|
||||
"""
|
||||
MetaProcessor - Tier 4: META Features (52 columns)
|
||||
|
||||
Long-term patterns and meta-features:
|
||||
- Stability (8 columns): volatility, recent form, win/loss rating
|
||||
- Side Preference (14 columns): CT vs T ratings, balance scores
|
||||
- Opponent Adaptation (12 columns): vs different ELO tiers
|
||||
- Map Specialization (10 columns): best/worst maps, versatility
|
||||
- Session Pattern (8 columns): daily/weekly patterns, streaks
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from typing import Dict, Any, List
|
||||
from .base_processor import BaseFeatureProcessor, SafeAggregator
|
||||
|
||||
|
||||
class MetaProcessor(BaseFeatureProcessor):
|
||||
"""Tier 4 META processor - Cross-match patterns and meta-analysis"""
|
||||
|
||||
MIN_MATCHES_REQUIRED = 15 # Need sufficient history for meta patterns
|
||||
|
||||
@staticmethod
|
||||
def calculate(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate all Tier 4 META features (52 columns)
|
||||
|
||||
Returns dict with keys starting with 'meta_'
|
||||
"""
|
||||
features = {}
|
||||
|
||||
# Check minimum matches
|
||||
if not BaseFeatureProcessor.check_min_matches(steam_id, conn_l2,
|
||||
MetaProcessor.MIN_MATCHES_REQUIRED):
|
||||
return _get_default_meta_features()
|
||||
|
||||
# Calculate each meta dimension
|
||||
features.update(MetaProcessor._calculate_stability(steam_id, conn_l2))
|
||||
features.update(MetaProcessor._calculate_side_preference(steam_id, conn_l2))
|
||||
features.update(MetaProcessor._calculate_opponent_adaptation(steam_id, conn_l2))
|
||||
features.update(MetaProcessor._calculate_map_specialization(steam_id, conn_l2))
|
||||
features.update(MetaProcessor._calculate_session_pattern(steam_id, conn_l2))
|
||||
|
||||
return features
|
||||
|
||||
@staticmethod
|
||||
def _calculate_stability(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate Stability (8 columns)
|
||||
|
||||
Columns:
|
||||
- meta_rating_volatility (STDDEV of last 20 matches)
|
||||
- meta_recent_form_rating (AVG of last 10 matches)
|
||||
- meta_win_rating, meta_loss_rating
|
||||
- meta_rating_consistency
|
||||
- meta_time_rating_correlation
|
||||
- meta_map_stability
|
||||
- meta_elo_tier_stability
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
# Get recent matches for volatility
|
||||
cursor.execute("""
|
||||
SELECT rating
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
ORDER BY match_id DESC
|
||||
LIMIT 20
|
||||
""", (steam_id,))
|
||||
|
||||
recent_ratings = [row[0] for row in cursor.fetchall() if row[0] is not None]
|
||||
|
||||
rating_volatility = SafeAggregator.safe_stddev(recent_ratings, 0.0)
|
||||
|
||||
# Recent form (last 10 matches)
|
||||
recent_form = SafeAggregator.safe_avg(recent_ratings[:10], 0.0) if len(recent_ratings) >= 10 else 0.0
|
||||
|
||||
# Win/loss ratings
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
AVG(CASE WHEN is_win = 1 THEN rating END) as win_rating,
|
||||
AVG(CASE WHEN is_win = 0 THEN rating END) as loss_rating
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
""", (steam_id,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
win_rating = row[0] if row[0] else 0.0
|
||||
loss_rating = row[1] if row[1] else 0.0
|
||||
|
||||
# Rating consistency (inverse of volatility, normalized)
|
||||
rating_consistency = max(0, 100 - (rating_volatility * 100))
|
||||
|
||||
# Time-rating correlation: calculate Pearson correlation between match time and rating
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
p.rating,
|
||||
m.start_time
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON p.match_id = m.match_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
AND p.rating IS NOT NULL
|
||||
AND m.start_time IS NOT NULL
|
||||
ORDER BY m.start_time
|
||||
""", (steam_id,))
|
||||
|
||||
time_rating_data = cursor.fetchall()
|
||||
|
||||
if len(time_rating_data) >= 2:
|
||||
ratings = [row[0] for row in time_rating_data]
|
||||
times = [row[1] for row in time_rating_data]
|
||||
|
||||
# Normalize timestamps to match indices
|
||||
time_indices = list(range(len(times)))
|
||||
|
||||
# Calculate Pearson correlation
|
||||
n = len(ratings)
|
||||
sum_x = sum(time_indices)
|
||||
sum_y = sum(ratings)
|
||||
sum_xy = sum(x * y for x, y in zip(time_indices, ratings))
|
||||
sum_x2 = sum(x * x for x in time_indices)
|
||||
sum_y2 = sum(y * y for y in ratings)
|
||||
|
||||
numerator = n * sum_xy - sum_x * sum_y
|
||||
denominator = ((n * sum_x2 - sum_x ** 2) * (n * sum_y2 - sum_y ** 2)) ** 0.5
|
||||
|
||||
time_rating_corr = SafeAggregator.safe_divide(numerator, denominator) if denominator > 0 else 0.0
|
||||
else:
|
||||
time_rating_corr = 0.0
|
||||
|
||||
# Map stability (STDDEV across maps)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
m.map_name,
|
||||
AVG(p.rating) as avg_rating
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON p.match_id = m.match_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
GROUP BY m.map_name
|
||||
""", (steam_id,))
|
||||
|
||||
map_ratings = [row[1] for row in cursor.fetchall() if row[1] is not None]
|
||||
map_stability = SafeAggregator.safe_stddev(map_ratings, 0.0)
|
||||
|
||||
# ELO tier stability (placeholder)
|
||||
elo_tier_stability = rating_volatility # Simplified
|
||||
|
||||
return {
|
||||
'meta_rating_volatility': round(rating_volatility, 3),
|
||||
'meta_recent_form_rating': round(recent_form, 3),
|
||||
'meta_win_rating': round(win_rating, 3),
|
||||
'meta_loss_rating': round(loss_rating, 3),
|
||||
'meta_rating_consistency': round(rating_consistency, 2),
|
||||
'meta_time_rating_correlation': round(time_rating_corr, 3),
|
||||
'meta_map_stability': round(map_stability, 3),
|
||||
'meta_elo_tier_stability': round(elo_tier_stability, 3),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_side_preference(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate Side Preference (14 columns)
|
||||
|
||||
Columns:
|
||||
- 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_preference
|
||||
- meta_side_balance_score
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
# Get CT side performance from fact_match_players_ct
|
||||
# Rating is now stored as rating2 from fight_ct
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
AVG(rating) as avg_rating,
|
||||
AVG(CAST(kills AS REAL) / NULLIF(deaths, 0)) as avg_kd,
|
||||
AVG(kast) as avg_kast,
|
||||
AVG(entry_kills) as avg_fk,
|
||||
SUM(CASE WHEN is_win = 1 THEN 1 ELSE 0 END) as wins,
|
||||
COUNT(*) as total_matches,
|
||||
SUM(round_total) as total_rounds
|
||||
FROM fact_match_players_ct
|
||||
WHERE steam_id_64 = ?
|
||||
AND rating IS NOT NULL AND rating > 0
|
||||
""", (steam_id,))
|
||||
|
||||
ct_row = cursor.fetchone()
|
||||
ct_rating = ct_row[0] if ct_row and ct_row[0] else 0.0
|
||||
ct_kd = ct_row[1] if ct_row and ct_row[1] else 0.0
|
||||
ct_kast = ct_row[2] if ct_row and ct_row[2] else 0.0
|
||||
ct_fk = ct_row[3] if ct_row and ct_row[3] else 0.0
|
||||
ct_wins = ct_row[4] if ct_row and ct_row[4] else 0
|
||||
ct_matches = ct_row[5] if ct_row and ct_row[5] else 1
|
||||
ct_rounds = ct_row[6] if ct_row and ct_row[6] else 1
|
||||
|
||||
ct_win_rate = SafeAggregator.safe_divide(ct_wins, ct_matches)
|
||||
ct_fk_rate = SafeAggregator.safe_divide(ct_fk, ct_rounds)
|
||||
|
||||
# Get T side performance from fact_match_players_t
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
AVG(rating) as avg_rating,
|
||||
AVG(CAST(kills AS REAL) / NULLIF(deaths, 0)) as avg_kd,
|
||||
AVG(kast) as avg_kast,
|
||||
AVG(entry_kills) as avg_fk,
|
||||
SUM(CASE WHEN is_win = 1 THEN 1 ELSE 0 END) as wins,
|
||||
COUNT(*) as total_matches,
|
||||
SUM(round_total) as total_rounds
|
||||
FROM fact_match_players_t
|
||||
WHERE steam_id_64 = ?
|
||||
AND rating IS NOT NULL AND rating > 0
|
||||
""", (steam_id,))
|
||||
|
||||
t_row = cursor.fetchone()
|
||||
t_rating = t_row[0] if t_row and t_row[0] else 0.0
|
||||
t_kd = t_row[1] if t_row and t_row[1] else 0.0
|
||||
t_kast = t_row[2] if t_row and t_row[2] else 0.0
|
||||
t_fk = t_row[3] if t_row and t_row[3] else 0.0
|
||||
t_wins = t_row[4] if t_row and t_row[4] else 0
|
||||
t_matches = t_row[5] if t_row and t_row[5] else 1
|
||||
t_rounds = t_row[6] if t_row and t_row[6] else 1
|
||||
|
||||
t_win_rate = SafeAggregator.safe_divide(t_wins, t_matches)
|
||||
t_fk_rate = SafeAggregator.safe_divide(t_fk, t_rounds)
|
||||
|
||||
# Differences
|
||||
rating_diff = ct_rating - t_rating
|
||||
kd_diff = ct_kd - t_kd
|
||||
|
||||
# Side preference classification
|
||||
if abs(rating_diff) < 0.05:
|
||||
side_preference = 'Balanced'
|
||||
elif rating_diff > 0:
|
||||
side_preference = 'CT'
|
||||
else:
|
||||
side_preference = 'T'
|
||||
|
||||
# Balance score (0-100, higher = more balanced)
|
||||
balance_score = max(0, 100 - abs(rating_diff) * 200)
|
||||
|
||||
return {
|
||||
'meta_side_ct_rating': round(ct_rating, 3),
|
||||
'meta_side_t_rating': round(t_rating, 3),
|
||||
'meta_side_ct_kd': round(ct_kd, 3),
|
||||
'meta_side_t_kd': round(t_kd, 3),
|
||||
'meta_side_ct_win_rate': round(ct_win_rate, 3),
|
||||
'meta_side_t_win_rate': round(t_win_rate, 3),
|
||||
'meta_side_ct_fk_rate': round(ct_fk_rate, 3),
|
||||
'meta_side_t_fk_rate': round(t_fk_rate, 3),
|
||||
'meta_side_ct_kast': round(ct_kast, 3),
|
||||
'meta_side_t_kast': round(t_kast, 3),
|
||||
'meta_side_rating_diff': round(rating_diff, 3),
|
||||
'meta_side_kd_diff': round(kd_diff, 3),
|
||||
'meta_side_preference': side_preference,
|
||||
'meta_side_balance_score': round(balance_score, 2),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_opponent_adaptation(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate Opponent Adaptation (12 columns)
|
||||
|
||||
ELO tiers: lower (<-200), similar (±200), higher (>+200)
|
||||
|
||||
Columns:
|
||||
- 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
|
||||
|
||||
NOTE: Using individual origin_elo from fact_match_players
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
# Get player's matches with individual ELO data
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
p.rating,
|
||||
CAST(p.kills AS REAL) / NULLIF(p.deaths, 0) as kd,
|
||||
p.is_win,
|
||||
p.origin_elo as player_elo,
|
||||
opp.avg_elo as opponent_avg_elo
|
||||
FROM fact_match_players p
|
||||
JOIN (
|
||||
SELECT
|
||||
match_id,
|
||||
team_id,
|
||||
AVG(origin_elo) as avg_elo
|
||||
FROM fact_match_players
|
||||
WHERE origin_elo IS NOT NULL
|
||||
GROUP BY match_id, team_id
|
||||
) opp ON p.match_id = opp.match_id AND p.team_id != opp.team_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
AND p.origin_elo IS NOT NULL
|
||||
""", (steam_id,))
|
||||
|
||||
matches = cursor.fetchall()
|
||||
|
||||
if not matches:
|
||||
return {
|
||||
'meta_opp_vs_lower_elo_rating': 0.0,
|
||||
'meta_opp_vs_lower_elo_kd': 0.0,
|
||||
'meta_opp_vs_similar_elo_rating': 0.0,
|
||||
'meta_opp_vs_similar_elo_kd': 0.0,
|
||||
'meta_opp_vs_higher_elo_rating': 0.0,
|
||||
'meta_opp_vs_higher_elo_kd': 0.0,
|
||||
'meta_opp_elo_adaptation': 0.0,
|
||||
'meta_opp_stomping_score': 0.0,
|
||||
'meta_opp_upset_score': 0.0,
|
||||
'meta_opp_consistency_across_elos': 0.0,
|
||||
'meta_opp_rank_resistance': 0.0,
|
||||
'meta_opp_smurf_detection': 0.0,
|
||||
}
|
||||
|
||||
# Categorize by ELO difference
|
||||
lower_elo_ratings = [] # Playing vs weaker opponents
|
||||
lower_elo_kds = []
|
||||
similar_elo_ratings = [] # Similar skill
|
||||
similar_elo_kds = []
|
||||
higher_elo_ratings = [] # Playing vs stronger opponents
|
||||
higher_elo_kds = []
|
||||
|
||||
stomping_score = 0 # Dominating weaker teams
|
||||
upset_score = 0 # Winning against stronger teams
|
||||
|
||||
for rating, kd, is_win, player_elo, opp_elo in matches:
|
||||
if rating is None or kd is None:
|
||||
continue
|
||||
|
||||
elo_diff = player_elo - opp_elo # Positive = we're stronger
|
||||
|
||||
# Categorize ELO tiers (±200 threshold)
|
||||
if elo_diff > 200: # We're stronger (opponent is lower ELO)
|
||||
lower_elo_ratings.append(rating)
|
||||
lower_elo_kds.append(kd)
|
||||
if is_win:
|
||||
stomping_score += 1
|
||||
elif elo_diff < -200: # Opponent is stronger (higher ELO)
|
||||
higher_elo_ratings.append(rating)
|
||||
higher_elo_kds.append(kd)
|
||||
if is_win:
|
||||
upset_score += 2 # Upset wins count more
|
||||
else: # Similar ELO (±200)
|
||||
similar_elo_ratings.append(rating)
|
||||
similar_elo_kds.append(kd)
|
||||
|
||||
# Calculate averages
|
||||
avg_lower_rating = SafeAggregator.safe_avg(lower_elo_ratings)
|
||||
avg_lower_kd = SafeAggregator.safe_avg(lower_elo_kds)
|
||||
avg_similar_rating = SafeAggregator.safe_avg(similar_elo_ratings)
|
||||
avg_similar_kd = SafeAggregator.safe_avg(similar_elo_kds)
|
||||
avg_higher_rating = SafeAggregator.safe_avg(higher_elo_ratings)
|
||||
avg_higher_kd = SafeAggregator.safe_avg(higher_elo_kds)
|
||||
|
||||
# ELO adaptation: performance improvement vs stronger opponents
|
||||
# Positive = performs better vs stronger teams (rare, good trait)
|
||||
elo_adaptation = avg_higher_rating - avg_lower_rating
|
||||
|
||||
# Consistency: std dev of ratings across ELO tiers
|
||||
all_tier_ratings = [avg_lower_rating, avg_similar_rating, avg_higher_rating]
|
||||
consistency = 100 - SafeAggregator.safe_stddev(all_tier_ratings) * 100
|
||||
|
||||
# Rank resistance: K/D vs higher ELO opponents
|
||||
rank_resistance = avg_higher_kd
|
||||
|
||||
# Smurf detection: high performance vs lower ELO
|
||||
# Indicators: rating > 1.15 AND kd > 1.2 when facing lower ELO opponents
|
||||
smurf_score = 0.0
|
||||
if len(lower_elo_ratings) > 0 and avg_lower_rating > 1.0:
|
||||
# Base score from rating dominance
|
||||
rating_bonus = max(0, (avg_lower_rating - 1.0) * 100)
|
||||
# Additional score from K/D dominance
|
||||
kd_bonus = max(0, (avg_lower_kd - 1.0) * 50)
|
||||
# Consistency bonus (more matches = more reliable indicator)
|
||||
consistency_bonus = min(len(lower_elo_ratings) / 5.0, 1.0) * 20
|
||||
|
||||
smurf_score = rating_bonus + kd_bonus + consistency_bonus
|
||||
|
||||
# Cap at 100
|
||||
smurf_score = min(smurf_score, 100.0)
|
||||
|
||||
return {
|
||||
'meta_opp_vs_lower_elo_rating': round(avg_lower_rating, 3),
|
||||
'meta_opp_vs_lower_elo_kd': round(avg_lower_kd, 3),
|
||||
'meta_opp_vs_similar_elo_rating': round(avg_similar_rating, 3),
|
||||
'meta_opp_vs_similar_elo_kd': round(avg_similar_kd, 3),
|
||||
'meta_opp_vs_higher_elo_rating': round(avg_higher_rating, 3),
|
||||
'meta_opp_vs_higher_elo_kd': round(avg_higher_kd, 3),
|
||||
'meta_opp_elo_adaptation': round(elo_adaptation, 3),
|
||||
'meta_opp_stomping_score': round(stomping_score, 2),
|
||||
'meta_opp_upset_score': round(upset_score, 2),
|
||||
'meta_opp_consistency_across_elos': round(consistency, 2),
|
||||
'meta_opp_rank_resistance': round(rank_resistance, 3),
|
||||
'meta_opp_smurf_detection': round(smurf_score, 2),
|
||||
}
|
||||
|
||||
# Performance vs lower ELO opponents (simplified - using match-level team ELO)
|
||||
# REMOVED DUPLICATE LOGIC BLOCK THAT WAS UNREACHABLE
|
||||
# The code previously had a return statement before this block, making it dead code.
|
||||
# Merged logic into the first block above using individual player ELOs which is more accurate.
|
||||
|
||||
@staticmethod
|
||||
def _calculate_map_specialization(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate Map Specialization (10 columns)
|
||||
|
||||
Columns:
|
||||
- meta_map_best_map, meta_map_best_rating
|
||||
- meta_map_worst_map, 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
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
# Map performance
|
||||
# Lower threshold to 1 match to ensure we catch high ratings even with low sample size
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
m.map_name,
|
||||
AVG(p.rating) as avg_rating,
|
||||
COUNT(*) as match_count
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON p.match_id = m.match_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
GROUP BY m.map_name
|
||||
HAVING match_count >= 1
|
||||
ORDER BY avg_rating DESC
|
||||
""", (steam_id,))
|
||||
|
||||
map_data = cursor.fetchall()
|
||||
|
||||
if not map_data:
|
||||
return {
|
||||
'meta_map_best_map': 'unknown',
|
||||
'meta_map_best_rating': 0.0,
|
||||
'meta_map_worst_map': 'unknown',
|
||||
'meta_map_worst_rating': 0.0,
|
||||
'meta_map_diversity': 0.0,
|
||||
'meta_map_pool_size': 0,
|
||||
'meta_map_specialist_score': 0.0,
|
||||
'meta_map_versatility': 0.0,
|
||||
'meta_map_comfort_zone_rate': 0.0,
|
||||
'meta_map_adaptation': 0.0,
|
||||
}
|
||||
|
||||
# Best map
|
||||
best_map = map_data[0][0]
|
||||
best_rating = map_data[0][1]
|
||||
|
||||
# Worst map
|
||||
worst_map = map_data[-1][0]
|
||||
worst_rating = map_data[-1][1]
|
||||
|
||||
# Map diversity (entropy-based)
|
||||
map_ratings = [row[1] for row in map_data]
|
||||
map_diversity = SafeAggregator.safe_stddev(map_ratings, 0.0)
|
||||
|
||||
# Map pool size (maps with 3+ matches, lowered from 5)
|
||||
cursor.execute("""
|
||||
SELECT COUNT(DISTINCT m.map_name)
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON p.match_id = m.match_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
GROUP BY m.map_name
|
||||
HAVING COUNT(*) >= 3
|
||||
""", (steam_id,))
|
||||
|
||||
pool_rows = cursor.fetchall()
|
||||
pool_size = len(pool_rows)
|
||||
|
||||
# Specialist score (difference between best and worst)
|
||||
specialist_score = best_rating - worst_rating
|
||||
|
||||
# Versatility (inverse of specialist score, normalized)
|
||||
versatility = max(0, 100 - specialist_score * 100)
|
||||
|
||||
# Comfort zone rate (% matches on top 3 maps)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(CASE WHEN m.map_name IN (
|
||||
SELECT map_name FROM (
|
||||
SELECT m2.map_name, COUNT(*) as cnt
|
||||
FROM fact_match_players p2
|
||||
JOIN fact_matches m2 ON p2.match_id = m2.match_id
|
||||
WHERE p2.steam_id_64 = ?
|
||||
GROUP BY m2.map_name
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 3
|
||||
)
|
||||
) THEN 1 ELSE 0 END) as comfort_matches,
|
||||
COUNT(*) as total_matches
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON p.match_id = m.match_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
""", (steam_id, steam_id))
|
||||
|
||||
comfort_row = cursor.fetchone()
|
||||
comfort_matches = comfort_row[0] if comfort_row[0] else 0
|
||||
total_matches = comfort_row[1] if comfort_row[1] else 1
|
||||
comfort_zone_rate = SafeAggregator.safe_divide(comfort_matches, total_matches)
|
||||
|
||||
# Map adaptation (avg rating on non-favorite maps)
|
||||
if len(map_data) > 1:
|
||||
non_favorite_ratings = [row[1] for row in map_data[1:]]
|
||||
map_adaptation = SafeAggregator.safe_avg(non_favorite_ratings, 0.0)
|
||||
else:
|
||||
map_adaptation = best_rating
|
||||
|
||||
return {
|
||||
'meta_map_best_map': best_map,
|
||||
'meta_map_best_rating': round(best_rating, 3),
|
||||
'meta_map_worst_map': worst_map,
|
||||
'meta_map_worst_rating': round(worst_rating, 3),
|
||||
'meta_map_diversity': round(map_diversity, 3),
|
||||
'meta_map_pool_size': pool_size,
|
||||
'meta_map_specialist_score': round(specialist_score, 3),
|
||||
'meta_map_versatility': round(versatility, 2),
|
||||
'meta_map_comfort_zone_rate': round(comfort_zone_rate, 3),
|
||||
'meta_map_adaptation': round(map_adaptation, 3),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_session_pattern(steam_id: str, conn_l2: sqlite3.Connection) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate Session Pattern (8 columns)
|
||||
|
||||
Columns:
|
||||
- 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
|
||||
|
||||
Note: Requires timestamp data in fact_matches
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
# Check if start_time exists
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) FROM fact_matches
|
||||
WHERE start_time IS NOT NULL AND start_time > 0
|
||||
LIMIT 1
|
||||
""")
|
||||
|
||||
has_timestamps = cursor.fetchone()[0] > 0
|
||||
|
||||
if not has_timestamps:
|
||||
# Return placeholder values
|
||||
return {
|
||||
'meta_session_avg_matches_per_day': 0.0,
|
||||
'meta_session_longest_streak': 0,
|
||||
'meta_session_weekend_rating': 0.0,
|
||||
'meta_session_weekday_rating': 0.0,
|
||||
'meta_session_morning_rating': 0.0,
|
||||
'meta_session_afternoon_rating': 0.0,
|
||||
'meta_session_evening_rating': 0.0,
|
||||
'meta_session_night_rating': 0.0,
|
||||
}
|
||||
|
||||
# 1. Matches per day
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
DATE(start_time, 'unixepoch') as match_date,
|
||||
COUNT(*) as daily_matches
|
||||
FROM fact_matches m
|
||||
JOIN fact_match_players p ON m.match_id = p.match_id
|
||||
WHERE p.steam_id_64 = ? AND m.start_time IS NOT NULL
|
||||
GROUP BY match_date
|
||||
""", (steam_id,))
|
||||
|
||||
daily_stats = cursor.fetchall()
|
||||
if daily_stats:
|
||||
avg_matches_per_day = sum(row[1] for row in daily_stats) / len(daily_stats)
|
||||
else:
|
||||
avg_matches_per_day = 0.0
|
||||
|
||||
# 2. Longest Streak (Consecutive wins)
|
||||
cursor.execute("""
|
||||
SELECT is_win
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON p.match_id = m.match_id
|
||||
WHERE p.steam_id_64 = ? AND m.start_time IS NOT NULL
|
||||
ORDER BY m.start_time
|
||||
""", (steam_id,))
|
||||
|
||||
results = cursor.fetchall()
|
||||
longest_streak = 0
|
||||
current_streak = 0
|
||||
for row in results:
|
||||
if row[0]: # Win
|
||||
current_streak += 1
|
||||
else:
|
||||
longest_streak = max(longest_streak, current_streak)
|
||||
current_streak = 0
|
||||
longest_streak = max(longest_streak, current_streak)
|
||||
|
||||
# 3. Time of Day & Week Analysis
|
||||
# Weekend: 0 (Sun) and 6 (Sat)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
CAST(strftime('%w', start_time, 'unixepoch') AS INTEGER) as day_of_week,
|
||||
CAST(strftime('%H', start_time, 'unixepoch') AS INTEGER) as hour_of_day,
|
||||
p.rating
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON p.match_id = m.match_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
AND m.start_time IS NOT NULL
|
||||
AND p.rating IS NOT NULL
|
||||
""", (steam_id,))
|
||||
|
||||
matches = cursor.fetchall()
|
||||
|
||||
weekend_ratings = []
|
||||
weekday_ratings = []
|
||||
morning_ratings = [] # 06-12
|
||||
afternoon_ratings = [] # 12-18
|
||||
evening_ratings = [] # 18-24
|
||||
night_ratings = [] # 00-06
|
||||
|
||||
for dow, hour, rating in matches:
|
||||
# Weekday/Weekend
|
||||
if dow == 0 or dow == 6:
|
||||
weekend_ratings.append(rating)
|
||||
else:
|
||||
weekday_ratings.append(rating)
|
||||
|
||||
# Time of Day
|
||||
if 6 <= hour < 12:
|
||||
morning_ratings.append(rating)
|
||||
elif 12 <= hour < 18:
|
||||
afternoon_ratings.append(rating)
|
||||
elif 18 <= hour <= 23:
|
||||
evening_ratings.append(rating)
|
||||
else: # 0-6
|
||||
night_ratings.append(rating)
|
||||
|
||||
return {
|
||||
'meta_session_avg_matches_per_day': round(avg_matches_per_day, 2),
|
||||
'meta_session_longest_streak': longest_streak,
|
||||
'meta_session_weekend_rating': round(SafeAggregator.safe_avg(weekend_ratings), 3),
|
||||
'meta_session_weekday_rating': round(SafeAggregator.safe_avg(weekday_ratings), 3),
|
||||
'meta_session_morning_rating': round(SafeAggregator.safe_avg(morning_ratings), 3),
|
||||
'meta_session_afternoon_rating': round(SafeAggregator.safe_avg(afternoon_ratings), 3),
|
||||
'meta_session_evening_rating': round(SafeAggregator.safe_avg(evening_ratings), 3),
|
||||
'meta_session_night_rating': round(SafeAggregator.safe_avg(night_ratings), 3),
|
||||
}
|
||||
|
||||
|
||||
def _get_default_meta_features() -> Dict[str, Any]:
|
||||
"""Return default zero values for all 52 META features"""
|
||||
return {
|
||||
# Stability (8)
|
||||
'meta_rating_volatility': 0.0,
|
||||
'meta_recent_form_rating': 0.0,
|
||||
'meta_win_rating': 0.0,
|
||||
'meta_loss_rating': 0.0,
|
||||
'meta_rating_consistency': 0.0,
|
||||
'meta_time_rating_correlation': 0.0,
|
||||
'meta_map_stability': 0.0,
|
||||
'meta_elo_tier_stability': 0.0,
|
||||
# Side Preference (14)
|
||||
'meta_side_ct_rating': 0.0,
|
||||
'meta_side_t_rating': 0.0,
|
||||
'meta_side_ct_kd': 0.0,
|
||||
'meta_side_t_kd': 0.0,
|
||||
'meta_side_ct_win_rate': 0.0,
|
||||
'meta_side_t_win_rate': 0.0,
|
||||
'meta_side_ct_fk_rate': 0.0,
|
||||
'meta_side_t_fk_rate': 0.0,
|
||||
'meta_side_ct_kast': 0.0,
|
||||
'meta_side_t_kast': 0.0,
|
||||
'meta_side_rating_diff': 0.0,
|
||||
'meta_side_kd_diff': 0.0,
|
||||
'meta_side_preference': 'Balanced',
|
||||
'meta_side_balance_score': 0.0,
|
||||
# Opponent Adaptation (12)
|
||||
'meta_opp_vs_lower_elo_rating': 0.0,
|
||||
'meta_opp_vs_similar_elo_rating': 0.0,
|
||||
'meta_opp_vs_higher_elo_rating': 0.0,
|
||||
'meta_opp_vs_lower_elo_kd': 0.0,
|
||||
'meta_opp_vs_similar_elo_kd': 0.0,
|
||||
'meta_opp_vs_higher_elo_kd': 0.0,
|
||||
'meta_opp_elo_adaptation': 0.0,
|
||||
'meta_opp_stomping_score': 0.0,
|
||||
'meta_opp_upset_score': 0.0,
|
||||
'meta_opp_consistency_across_elos': 0.0,
|
||||
'meta_opp_rank_resistance': 0.0,
|
||||
'meta_opp_smurf_detection': 0.0,
|
||||
# Map Specialization (10)
|
||||
'meta_map_best_map': 'unknown',
|
||||
'meta_map_best_rating': 0.0,
|
||||
'meta_map_worst_map': 'unknown',
|
||||
'meta_map_worst_rating': 0.0,
|
||||
'meta_map_diversity': 0.0,
|
||||
'meta_map_pool_size': 0,
|
||||
'meta_map_specialist_score': 0.0,
|
||||
'meta_map_versatility': 0.0,
|
||||
'meta_map_comfort_zone_rate': 0.0,
|
||||
'meta_map_adaptation': 0.0,
|
||||
# Session Pattern (8)
|
||||
'meta_session_avg_matches_per_day': 0.0,
|
||||
'meta_session_longest_streak': 0,
|
||||
'meta_session_weekend_rating': 0.0,
|
||||
'meta_session_weekday_rating': 0.0,
|
||||
'meta_session_morning_rating': 0.0,
|
||||
'meta_session_afternoon_rating': 0.0,
|
||||
'meta_session_evening_rating': 0.0,
|
||||
'meta_session_night_rating': 0.0,
|
||||
}
|
||||
Reference in New Issue
Block a user