Files
clutch/database/L3/processors/meta_processor.py

721 lines
28 KiB
Python
Raw Normal View History

"""
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,
}