2.0.0 Alpha: Data Refinery
This commit is contained in:
Binary file not shown.
+490
-12
@@ -6,6 +6,8 @@ import sqlite3
|
||||
import json
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
from collections import defaultdict, deque
|
||||
from typing import Optional
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
@@ -15,10 +17,14 @@ logger = logging.getLogger(__name__)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Points to database/ directory
|
||||
PROJECT_ROOT = os.path.dirname(BASE_DIR) # Points to project root
|
||||
sys.path.insert(0, PROJECT_ROOT) # Add project root to Python path
|
||||
L2_DB_PATH = os.path.join(BASE_DIR, 'L2', 'L2.db')
|
||||
L3_DB_PATH = os.path.join(BASE_DIR, 'L3', 'L3.db')
|
||||
WEB_DB_PATH = os.path.join(BASE_DIR, 'Web', 'Web_App.sqlite')
|
||||
SCHEMA_PATH = os.path.join(BASE_DIR, 'L3', 'schema.sql')
|
||||
|
||||
from database.paths import L2_DB, L3_DB, L3_SCHEMA, WEB_DB
|
||||
|
||||
L2_DB_PATH = str(L2_DB)
|
||||
L3_DB_PATH = str(L3_DB)
|
||||
L3_BACKUP_PATH = f"{L3_DB_PATH}.bak"
|
||||
WEB_DB_PATH = str(WEB_DB)
|
||||
SCHEMA_PATH = str(L3_SCHEMA)
|
||||
|
||||
def _get_existing_columns(conn, table_name):
|
||||
cur = conn.execute(f"PRAGMA table_info({table_name})")
|
||||
@@ -76,7 +82,28 @@ def _get_team_players():
|
||||
try:
|
||||
conn = sqlite3.connect(WEB_DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT player_ids_json FROM team_lineups")
|
||||
columns = {
|
||||
row[1] for row in cursor.execute("PRAGMA table_info(team_lineups)")
|
||||
}
|
||||
if 'is_active' in columns:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT player_ids_json
|
||||
FROM team_lineups
|
||||
WHERE is_active = 1
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT player_ids_json
|
||||
FROM team_lineups
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
steam_ids = set()
|
||||
@@ -150,7 +177,25 @@ def _build_player_record(steam_id: str):
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
def main(force_all: bool = False, workers: int = 1):
|
||||
def _backup_l3_database(source_path=L3_DB_PATH, backup_path=L3_BACKUP_PATH):
|
||||
if not os.path.exists(source_path):
|
||||
return None
|
||||
|
||||
source = sqlite3.connect(source_path)
|
||||
backup = sqlite3.connect(backup_path)
|
||||
try:
|
||||
source.backup(backup)
|
||||
result = backup.execute("PRAGMA quick_check").fetchone()[0]
|
||||
if result != 'ok':
|
||||
raise RuntimeError(f"L3 backup quick_check failed: {result}")
|
||||
finally:
|
||||
source.close()
|
||||
backup.close()
|
||||
logger.info("L3 backup created at %s", backup_path)
|
||||
return backup_path
|
||||
|
||||
|
||||
def main(force_all: bool = False, workers: int = 1, create_backup: bool = True):
|
||||
"""
|
||||
Main L3 feature building pipeline using modular processors
|
||||
"""
|
||||
@@ -158,6 +203,9 @@ def main(force_all: bool = False, workers: int = 1):
|
||||
logger.info("Starting L3 Builder with 5-Tier Architecture")
|
||||
logger.info("========================================")
|
||||
|
||||
if create_backup:
|
||||
_backup_l3_database()
|
||||
|
||||
# 1. Ensure Schema is up to date
|
||||
init_db()
|
||||
|
||||
@@ -181,6 +229,7 @@ def main(force_all: bool = False, workers: int = 1):
|
||||
conn_l3 = sqlite3.connect(L3_DB_PATH)
|
||||
|
||||
try:
|
||||
conn_l3.execute("BEGIN IMMEDIATE")
|
||||
cursor_l2 = conn_l2.cursor()
|
||||
if force_all:
|
||||
logger.info("Force mode enabled: building L3 for all players in L2.")
|
||||
@@ -240,7 +289,6 @@ def main(force_all: bool = False, workers: int = 1):
|
||||
)
|
||||
success_count += 1
|
||||
if processed_count % 2 == 0:
|
||||
conn_l3.commit()
|
||||
logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)")
|
||||
else:
|
||||
for idx, row in enumerate(players, 1):
|
||||
@@ -268,10 +316,20 @@ def main(force_all: bool = False, workers: int = 1):
|
||||
|
||||
processed_count = idx
|
||||
if processed_count % 2 == 0:
|
||||
conn_l3.commit()
|
||||
logger.info(f"Progress: {processed_count}/{total_players} ({success_count} success, {error_count} errors)")
|
||||
|
||||
# Final commit
|
||||
|
||||
if error_count:
|
||||
raise RuntimeError(
|
||||
f"L3 feature build failed for {error_count}/{total_players} players"
|
||||
)
|
||||
|
||||
processed_ids = [str(row[0]) for row in players]
|
||||
_update_percentiles(conn_l3, processed_ids)
|
||||
_rebuild_auxiliary_marts(conn_l2, conn_l3, processed_ids)
|
||||
|
||||
quick_check = conn_l3.execute("PRAGMA quick_check").fetchone()[0]
|
||||
if quick_check != 'ok':
|
||||
raise RuntimeError(f"L3 quick_check failed before commit: {quick_check}")
|
||||
conn_l3.commit()
|
||||
|
||||
logger.info("========================================")
|
||||
@@ -283,9 +341,11 @@ def main(force_all: bool = False, workers: int = 1):
|
||||
logger.info("========================================")
|
||||
|
||||
except Exception as e:
|
||||
conn_l3.rollback()
|
||||
logger.error(f"Fatal error during L3 build: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
finally:
|
||||
conn_l2.close()
|
||||
@@ -313,7 +373,7 @@ def _get_round_count(steam_id: str, conn_l2: sqlite3.Connection) -> int:
|
||||
|
||||
|
||||
def _upsert_features(conn_l3: sqlite3.Connection, steam_id: str, features: dict,
|
||||
match_count: int, round_count: int, conn_l2: sqlite3.Connection | None,
|
||||
match_count: int, round_count: int, conn_l2: Optional[sqlite3.Connection],
|
||||
first_match_date=None, last_match_date=None):
|
||||
"""
|
||||
Insert or update player features in dm_player_features
|
||||
@@ -353,12 +413,430 @@ def _upsert_features(conn_l3: sqlite3.Connection, steam_id: str, features: dict,
|
||||
|
||||
cursor_l3.execute(sql, values)
|
||||
|
||||
|
||||
def _rebuild_auxiliary_marts(conn_l2, conn_l3, steam_ids):
|
||||
"""Rebuild player-grain marts used by profiles and trend APIs."""
|
||||
if not steam_ids:
|
||||
return
|
||||
|
||||
logger.info("Rebuilding L3 match, map and weapon marts")
|
||||
total_history = 0
|
||||
total_maps = 0
|
||||
total_weapons = 0
|
||||
total_periods = 0
|
||||
total_records = 0
|
||||
|
||||
for start in range(0, len(steam_ids), 400):
|
||||
chunk = steam_ids[start:start + 400]
|
||||
placeholders = ','.join('?' for _ in chunk)
|
||||
|
||||
for table in (
|
||||
'dm_player_match_history',
|
||||
'dm_player_map_stats',
|
||||
'dm_player_weapon_stats',
|
||||
'dm_player_period_stats',
|
||||
'dm_player_records',
|
||||
):
|
||||
conn_l3.execute(
|
||||
f"DELETE FROM {table} WHERE steam_id_64 IN ({placeholders})",
|
||||
chunk,
|
||||
)
|
||||
|
||||
history_rows = conn_l2.execute(
|
||||
f"""
|
||||
SELECT
|
||||
mp.steam_id_64,
|
||||
mp.match_id,
|
||||
m.start_time,
|
||||
mp.rating,
|
||||
mp.kd_ratio,
|
||||
mp.adr,
|
||||
mp.kast,
|
||||
mp.is_win,
|
||||
m.map_name,
|
||||
mp.kills,
|
||||
mp.deaths,
|
||||
mp.headshot_count,
|
||||
(
|
||||
SELECT AVG(teammate.rating)
|
||||
FROM fact_match_players teammate
|
||||
WHERE teammate.match_id = mp.match_id
|
||||
AND teammate.team_id = mp.team_id
|
||||
AND teammate.steam_id_64 != mp.steam_id_64
|
||||
) AS teammate_avg_rating
|
||||
FROM fact_match_players mp
|
||||
JOIN fact_matches m ON m.match_id = mp.match_id
|
||||
WHERE mp.steam_id_64 IN ({placeholders})
|
||||
ORDER BY mp.steam_id_64, m.start_time, mp.match_id
|
||||
""",
|
||||
chunk,
|
||||
).fetchall()
|
||||
|
||||
history_values = []
|
||||
player_state = defaultdict(lambda: {
|
||||
'sequence': 0,
|
||||
'rating_sum': 0.0,
|
||||
'recent': deque(maxlen=10),
|
||||
})
|
||||
for row in history_rows:
|
||||
steam_id = str(row[0])
|
||||
state = player_state[steam_id]
|
||||
rating = float(row[3] or 0.0)
|
||||
state['sequence'] += 1
|
||||
state['rating_sum'] += rating
|
||||
state['recent'].append(rating)
|
||||
history_values.append((
|
||||
steam_id,
|
||||
row[1],
|
||||
row[2],
|
||||
state['sequence'],
|
||||
row[3],
|
||||
row[4],
|
||||
row[5],
|
||||
row[6],
|
||||
row[7],
|
||||
row[8],
|
||||
None,
|
||||
row[12],
|
||||
state['rating_sum'] / state['sequence'],
|
||||
sum(state['recent']) / len(state['recent']),
|
||||
))
|
||||
|
||||
conn_l3.executemany(
|
||||
"""
|
||||
INSERT INTO dm_player_match_history (
|
||||
steam_id_64, match_id, match_date, match_sequence,
|
||||
rating, kd_ratio, adr, kast, is_win, map_name,
|
||||
opponent_avg_elo, teammate_avg_rating,
|
||||
cumulative_rating, rolling_10_rating
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
history_values,
|
||||
)
|
||||
total_history += len(history_values)
|
||||
|
||||
map_rows = conn_l2.execute(
|
||||
f"""
|
||||
SELECT
|
||||
mp.steam_id_64,
|
||||
m.map_name,
|
||||
COUNT(*) AS matches,
|
||||
SUM(CASE WHEN mp.is_win = 1 THEN 1 ELSE 0 END) AS wins,
|
||||
AVG(mp.rating) AS avg_rating,
|
||||
AVG(mp.kd_ratio) AS avg_kd,
|
||||
AVG(mp.adr) AS avg_adr,
|
||||
AVG(mp.kast) AS avg_kast,
|
||||
MAX(mp.rating) AS best_rating,
|
||||
MIN(mp.rating) AS worst_rating
|
||||
FROM fact_match_players mp
|
||||
JOIN fact_matches m ON m.match_id = mp.match_id
|
||||
WHERE mp.steam_id_64 IN ({placeholders})
|
||||
AND m.map_name IS NOT NULL
|
||||
AND m.map_name != ''
|
||||
GROUP BY mp.steam_id_64, m.map_name
|
||||
""",
|
||||
chunk,
|
||||
).fetchall()
|
||||
map_values = [
|
||||
tuple(row[:4]) + (
|
||||
(row[3] or 0) / row[2] if row[2] else 0.0,
|
||||
) + tuple(row[4:])
|
||||
for row in map_rows
|
||||
]
|
||||
conn_l3.executemany(
|
||||
"""
|
||||
INSERT INTO dm_player_map_stats (
|
||||
steam_id_64, map_name, matches, wins, win_rate,
|
||||
avg_rating, avg_kd, avg_adr, avg_kast,
|
||||
best_rating, worst_rating
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
map_values,
|
||||
)
|
||||
total_maps += len(map_values)
|
||||
|
||||
round_counts = {
|
||||
str(row[0]): int(row[1] or 0)
|
||||
for row in conn_l2.execute(
|
||||
f"""
|
||||
SELECT steam_id_64, SUM(round_total)
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 IN ({placeholders})
|
||||
GROUP BY steam_id_64
|
||||
""",
|
||||
chunk,
|
||||
)
|
||||
}
|
||||
weapon_rows = conn_l2.execute(
|
||||
f"""
|
||||
SELECT
|
||||
attacker_steam_id,
|
||||
weapon,
|
||||
COUNT(*) AS total_kills,
|
||||
SUM(CASE WHEN is_headshot = 1 THEN 1 ELSE 0 END) AS total_headshots,
|
||||
COUNT(DISTINCT match_id || ':' || round_num) AS usage_rounds
|
||||
FROM fact_round_events
|
||||
WHERE event_type = 'kill'
|
||||
AND attacker_steam_id IN ({placeholders})
|
||||
AND weapon IS NOT NULL
|
||||
AND weapon != ''
|
||||
GROUP BY attacker_steam_id, weapon
|
||||
""",
|
||||
chunk,
|
||||
).fetchall()
|
||||
weapon_values = []
|
||||
for row in weapon_rows:
|
||||
rounds = round_counts.get(str(row[0]), 0)
|
||||
kills = int(row[2] or 0)
|
||||
headshots = int(row[3] or 0)
|
||||
usage_rounds = int(row[4] or 0)
|
||||
hs_rate = headshots / kills if kills else 0.0
|
||||
usage_rate = usage_rounds / rounds if rounds else 0.0
|
||||
kills_per_round = kills / rounds if rounds else 0.0
|
||||
effectiveness = kills / usage_rounds if usage_rounds else 0.0
|
||||
weapon_values.append((
|
||||
str(row[0]),
|
||||
row[1],
|
||||
kills,
|
||||
headshots,
|
||||
hs_rate,
|
||||
usage_rounds,
|
||||
usage_rate,
|
||||
kills_per_round,
|
||||
effectiveness,
|
||||
))
|
||||
conn_l3.executemany(
|
||||
"""
|
||||
INSERT INTO dm_player_weapon_stats (
|
||||
steam_id_64, weapon_name, total_kills, total_headshots,
|
||||
hs_rate, usage_rounds, usage_rate,
|
||||
avg_kills_per_round, effectiveness_score
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
weapon_values,
|
||||
)
|
||||
total_weapons += len(weapon_values)
|
||||
|
||||
period_values = _calculate_period_rows(history_rows)
|
||||
conn_l3.executemany(
|
||||
"""
|
||||
INSERT INTO dm_player_period_stats (
|
||||
steam_id_64, period_key, period_label,
|
||||
period_start, period_end, matches, wins, win_rate,
|
||||
avg_rating, avg_kd, avg_adr, avg_kast,
|
||||
total_kills, total_deaths, sample_reliable
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
period_values,
|
||||
)
|
||||
total_periods += len(period_values)
|
||||
|
||||
record_values = _calculate_record_rows(history_rows)
|
||||
conn_l3.executemany(
|
||||
"""
|
||||
INSERT INTO dm_player_records (
|
||||
steam_id_64, record_key, record_label, record_value,
|
||||
match_id, map_name, match_date
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
record_values,
|
||||
)
|
||||
total_records += len(record_values)
|
||||
|
||||
logger.info(
|
||||
"Auxiliary marts rebuilt: %s history, %s map, %s weapon, "
|
||||
"%s period, %s record rows",
|
||||
total_history,
|
||||
total_maps,
|
||||
total_weapons,
|
||||
total_periods,
|
||||
total_records,
|
||||
)
|
||||
|
||||
|
||||
def _group_player_match_rows(history_rows):
|
||||
grouped = defaultdict(list)
|
||||
for row in history_rows:
|
||||
grouped[str(row['steam_id_64'])].append(row)
|
||||
for rows in grouped.values():
|
||||
rows.sort(key=lambda row: (row['start_time'] or 0, row['match_id']))
|
||||
return grouped
|
||||
|
||||
|
||||
def _safe_average(rows, key):
|
||||
values = [float(row[key]) for row in rows if row[key] is not None]
|
||||
return sum(values) / len(values) if values else None
|
||||
|
||||
|
||||
def _calculate_period_rows(history_rows):
|
||||
result = []
|
||||
for steam_id, all_rows in _group_player_match_rows(history_rows).items():
|
||||
latest_time = max(int(row['start_time'] or 0) for row in all_rows)
|
||||
period_groups = [
|
||||
('career', '生涯', all_rows),
|
||||
('last_10', '最近 10 场', all_rows[-10:]),
|
||||
('last_20', '最近 20 场', all_rows[-20:]),
|
||||
('last_30', '最近 30 场', all_rows[-30:]),
|
||||
(
|
||||
'days_30',
|
||||
'最近 30 天',
|
||||
[
|
||||
row for row in all_rows
|
||||
if int(row['start_time'] or 0) >= latest_time - 30 * 86400
|
||||
],
|
||||
),
|
||||
(
|
||||
'days_90',
|
||||
'最近 90 天',
|
||||
[
|
||||
row for row in all_rows
|
||||
if int(row['start_time'] or 0) >= latest_time - 90 * 86400
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
for period_key, period_label, rows in period_groups:
|
||||
if not rows:
|
||||
continue
|
||||
matches = len(rows)
|
||||
wins = sum(1 for row in rows if row['is_win'])
|
||||
kills = sum(int(row['kills'] or 0) for row in rows)
|
||||
deaths = sum(int(row['deaths'] or 0) for row in rows)
|
||||
result.append((
|
||||
steam_id,
|
||||
period_key,
|
||||
period_label,
|
||||
min(int(row['start_time'] or 0) for row in rows),
|
||||
max(int(row['start_time'] or 0) for row in rows),
|
||||
matches,
|
||||
wins,
|
||||
wins / matches,
|
||||
_safe_average(rows, 'rating'),
|
||||
kills / deaths if deaths else float(kills),
|
||||
_safe_average(rows, 'adr'),
|
||||
_safe_average(rows, 'kast'),
|
||||
kills,
|
||||
deaths,
|
||||
1 if matches >= 10 else 0,
|
||||
))
|
||||
return result
|
||||
|
||||
|
||||
def _calculate_record_rows(history_rows):
|
||||
result = []
|
||||
metric_definitions = (
|
||||
('highest_rating', '最高 Rating', 'rating'),
|
||||
('most_kills', '单场最多击杀', 'kills'),
|
||||
('highest_adr', '单场最高 ADR', 'adr'),
|
||||
('highest_kd', '单场最高 K/D', 'kd_ratio'),
|
||||
('most_headshots', '单场最多爆头', 'headshot_count'),
|
||||
)
|
||||
|
||||
for steam_id, rows in _group_player_match_rows(history_rows).items():
|
||||
for record_key, record_label, field in metric_definitions:
|
||||
candidates = [row for row in rows if row[field] is not None]
|
||||
if not candidates:
|
||||
continue
|
||||
best = max(
|
||||
candidates,
|
||||
key=lambda row: (
|
||||
float(row[field]),
|
||||
int(row['start_time'] or 0),
|
||||
),
|
||||
)
|
||||
result.append((
|
||||
steam_id,
|
||||
record_key,
|
||||
record_label,
|
||||
float(best[field]),
|
||||
best['match_id'],
|
||||
best['map_name'],
|
||||
best['start_time'],
|
||||
))
|
||||
|
||||
longest_streak = 0
|
||||
current_streak = 0
|
||||
streak_end = None
|
||||
for row in rows:
|
||||
if row['is_win']:
|
||||
current_streak += 1
|
||||
if current_streak >= longest_streak:
|
||||
longest_streak = current_streak
|
||||
streak_end = row
|
||||
else:
|
||||
current_streak = 0
|
||||
if streak_end is not None:
|
||||
result.append((
|
||||
steam_id,
|
||||
'longest_win_streak',
|
||||
'最长连胜',
|
||||
float(longest_streak),
|
||||
streak_end['match_id'],
|
||||
streak_end['map_name'],
|
||||
streak_end['start_time'],
|
||||
))
|
||||
return result
|
||||
|
||||
|
||||
def _update_percentiles(conn_l3, steam_ids):
|
||||
"""Calculate a real percentile among eligible players in this build."""
|
||||
if not steam_ids:
|
||||
return
|
||||
|
||||
score_rows = []
|
||||
for start in range(0, len(steam_ids), 400):
|
||||
chunk = steam_ids[start:start + 400]
|
||||
placeholders = ','.join('?' for _ in chunk)
|
||||
conn_l3.execute(
|
||||
f"""
|
||||
UPDATE dm_player_features
|
||||
SET tier_percentile = NULL
|
||||
WHERE steam_id_64 IN ({placeholders})
|
||||
""",
|
||||
chunk,
|
||||
)
|
||||
score_rows.extend(conn_l3.execute(
|
||||
f"""
|
||||
SELECT steam_id_64, score_overall
|
||||
FROM dm_player_features
|
||||
WHERE steam_id_64 IN ({placeholders})
|
||||
AND score_overall > 0
|
||||
""",
|
||||
chunk,
|
||||
).fetchall())
|
||||
|
||||
if not score_rows:
|
||||
return
|
||||
|
||||
scores = [float(row[1]) for row in score_rows]
|
||||
percentile_values = []
|
||||
for row in score_rows:
|
||||
score = float(row[1])
|
||||
percentile = sum(value <= score for value in scores) / len(scores) * 100
|
||||
percentile_values.append((round(percentile, 2), str(row[0])))
|
||||
|
||||
conn_l3.executemany(
|
||||
"""
|
||||
UPDATE dm_player_features
|
||||
SET tier_percentile = ?
|
||||
WHERE steam_id_64 = ?
|
||||
""",
|
||||
percentile_values,
|
||||
)
|
||||
logger.info("Updated percentiles for %s eligible players", len(score_rows))
|
||||
|
||||
|
||||
def _parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--force", action="store_true")
|
||||
parser.add_argument("--workers", type=int, default=1)
|
||||
parser.add_argument("--no-backup", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = _parse_args()
|
||||
main(force_all=args.force, workers=args.workers)
|
||||
main(
|
||||
force_all=args.force,
|
||||
workers=args.workers,
|
||||
create_backup=not args.no_backup,
|
||||
)
|
||||
|
||||
@@ -65,8 +65,8 @@ class CompositeProcessor(BaseFeatureProcessor):
|
||||
# Classify tier based on overall score
|
||||
features['tier_classification'] = CompositeProcessor._classify_tier(features['score_overall'])
|
||||
|
||||
# Percentile rank (placeholder - requires all players)
|
||||
features['tier_percentile'] = min(features['score_overall'], 100.0)
|
||||
# Filled by L3_Builder after every eligible player has been calculated.
|
||||
features['tier_percentile'] = None
|
||||
|
||||
return features
|
||||
|
||||
@@ -266,13 +266,13 @@ class CompositeProcessor(BaseFeatureProcessor):
|
||||
STABILITY Score (0-100) | 8%
|
||||
"""
|
||||
# Extract features
|
||||
volatility = features.get('meta_rating_volatility', 0.0)
|
||||
loss_rating = features.get('meta_loss_rating', 0.0)
|
||||
consistency = features.get('meta_rating_consistency', 0.0)
|
||||
tilt_resilience = features.get('int_pressure_tilt_resistance', 0.0)
|
||||
map_stable = features.get('meta_map_stability', 0.0)
|
||||
elo_stable = features.get('meta_elo_tier_stability', 0.0)
|
||||
recent_form = features.get('meta_recent_form_rating', 0.0)
|
||||
volatility = features.get('meta_rating_volatility') or 0.0
|
||||
loss_rating = features.get('meta_loss_rating') or 0.0
|
||||
consistency = features.get('meta_rating_consistency') or 0.0
|
||||
tilt_resilience = features.get('int_pressure_tilt_resistance') or 0.0
|
||||
map_stable = features.get('meta_map_stability') or 0.0
|
||||
elo_stable = features.get('meta_elo_tier_stability') or 0.0
|
||||
recent_form = features.get('meta_recent_form_rating') or 0.0
|
||||
|
||||
# Normalize
|
||||
# Volatility: Reverse score. 100 - (Vol * 220)
|
||||
@@ -281,8 +281,8 @@ class CompositeProcessor(BaseFeatureProcessor):
|
||||
loss_score = min((loss_rating / 1.00) * 100, 100)
|
||||
cons_score = min((consistency / 70) * 100, 100)
|
||||
tilt_score = min((tilt_resilience / 0.80) * 100, 100)
|
||||
map_score = min((map_stable / 0.25) * 100, 100)
|
||||
elo_score = min((elo_stable / 0.48) * 100, 100)
|
||||
map_score = max(0, min(100, 100 - (map_stable / 0.25) * 100))
|
||||
elo_score = max(0, min(100, 100 - (elo_stable / 0.48) * 100))
|
||||
recent_score = min((recent_form / 1.15) * 100, 100)
|
||||
|
||||
# Weighted Sum
|
||||
@@ -337,12 +337,12 @@ class CompositeProcessor(BaseFeatureProcessor):
|
||||
PACE Score (0-100) | 5%
|
||||
"""
|
||||
# Extract features
|
||||
early_kill_pct = features.get('int_timing_early_kill_share', 0.0)
|
||||
aggression = features.get('int_timing_aggression_index', 0.0)
|
||||
trade_speed = features.get('int_trade_response_time', 0.0)
|
||||
trade_kill = features.get('int_trade_kill_count', 0)
|
||||
teamwork = features.get('int_teamwork_score', 0.0)
|
||||
first_contact = features.get('int_timing_first_contact_time', 0.0)
|
||||
early_kill_pct = features.get('int_timing_early_kill_share') or 0.0
|
||||
aggression = features.get('int_timing_aggression_index') or 0.0
|
||||
trade_speed = features.get('int_trade_response_time') or 0.0
|
||||
trade_kill = features.get('int_trade_kill_count') or 0
|
||||
teamwork = features.get('int_teamwork_score') or 0.0
|
||||
first_contact = features.get('int_timing_first_contact_time') or 0.0
|
||||
|
||||
# Normalize
|
||||
early_score = min((early_kill_pct / 0.44) * 100, 100)
|
||||
@@ -353,7 +353,7 @@ class CompositeProcessor(BaseFeatureProcessor):
|
||||
if trade_speed > 0.01:
|
||||
trade_speed_score = min((2.0 / trade_speed) * 100, 100)
|
||||
else:
|
||||
trade_speed_score = 100 # Instant trade
|
||||
trade_speed_score = 0
|
||||
|
||||
trade_kill_score = min((trade_kill / 650) * 100, 100)
|
||||
teamwork_score = min((teamwork / 29) * 100, 100)
|
||||
@@ -362,13 +362,7 @@ class CompositeProcessor(BaseFeatureProcessor):
|
||||
if first_contact > 0.01:
|
||||
first_contact_score = min((30 / first_contact) * 100, 100)
|
||||
else:
|
||||
first_contact_score = 0 # If 0, probably no data, safe to say 0? Or 100?
|
||||
# 0 first contact time means instant damage.
|
||||
# But "30 / Contact" means smaller contact time gives higher score.
|
||||
# If contact time is 0, score explodes.
|
||||
# Realistically first contact time is > 0.
|
||||
# I will clamp it.
|
||||
first_contact_score = 100 # Assume very fast
|
||||
first_contact_score = 0
|
||||
|
||||
# Weighted Sum
|
||||
pace_score = (
|
||||
@@ -416,5 +410,5 @@ def _get_default_composite_features() -> Dict[str, Any]:
|
||||
'score_pace': 0.0,
|
||||
'score_overall': 0.0,
|
||||
'tier_classification': 'Beginner',
|
||||
'tier_percentile': 0.0,
|
||||
'tier_percentile': None,
|
||||
}
|
||||
|
||||
@@ -466,7 +466,8 @@ class IntelligenceProcessor(BaseFeatureProcessor):
|
||||
- int_pos_spatial_iq_score
|
||||
- int_pos_avg_distance_from_teammates
|
||||
|
||||
Note: Simplified implementation - full version requires DBSCAN clustering
|
||||
Only geometry-independent values are calculated here. Metrics that
|
||||
require map boundaries, paths or teammate positions remain NULL.
|
||||
"""
|
||||
cursor = conn_l2.cursor()
|
||||
|
||||
@@ -481,26 +482,23 @@ class IntelligenceProcessor(BaseFeatureProcessor):
|
||||
has_position_data = cursor.fetchone()[0] > 0
|
||||
|
||||
if not has_position_data:
|
||||
# Return placeholder values if no position data
|
||||
return {
|
||||
'int_pos_site_a_control_rate': 0.0,
|
||||
'int_pos_site_b_control_rate': 0.0,
|
||||
'int_pos_mid_control_rate': 0.0,
|
||||
'int_pos_favorite_position': 'unknown',
|
||||
'int_pos_position_diversity': 0.0,
|
||||
'int_pos_rotation_speed': 0.0,
|
||||
'int_pos_map_coverage': 0.0,
|
||||
'int_pos_lurk_tendency': 0.0,
|
||||
'int_pos_site_anchor_score': 0.0,
|
||||
'int_pos_entry_route_diversity': 0.0,
|
||||
'int_pos_retake_positioning': 0.0,
|
||||
'int_pos_postplant_positioning': 0.0,
|
||||
'int_pos_spatial_iq_score': 0.0,
|
||||
'int_pos_avg_distance_from_teammates': 0.0,
|
||||
'int_pos_site_a_control_rate': None,
|
||||
'int_pos_site_b_control_rate': None,
|
||||
'int_pos_mid_control_rate': None,
|
||||
'int_pos_favorite_position': None,
|
||||
'int_pos_position_diversity': None,
|
||||
'int_pos_rotation_speed': None,
|
||||
'int_pos_map_coverage': None,
|
||||
'int_pos_lurk_tendency': None,
|
||||
'int_pos_site_anchor_score': None,
|
||||
'int_pos_entry_route_diversity': None,
|
||||
'int_pos_retake_positioning': None,
|
||||
'int_pos_postplant_positioning': None,
|
||||
'int_pos_spatial_iq_score': None,
|
||||
'int_pos_avg_distance_from_teammates': None,
|
||||
}
|
||||
|
||||
# Simplified position analysis (proper implementation needs clustering)
|
||||
# Calculate basic position variance as proxy for mobility
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
AVG(attacker_pos_x) as avg_x,
|
||||
@@ -515,34 +513,24 @@ class IntelligenceProcessor(BaseFeatureProcessor):
|
||||
pos_row = cursor.fetchone()
|
||||
position_count = pos_row[3] if pos_row[3] else 1
|
||||
|
||||
# Position diversity based on unique grid cells visited
|
||||
position_diversity = min(position_count / 50.0, 1.0) # Normalize to 0-1
|
||||
|
||||
# Map coverage (simplified)
|
||||
map_coverage = position_diversity
|
||||
|
||||
# Site control rates CANNOT be calculated without map-specific geometry data
|
||||
# Each map (Dust2, Mirage, Nuke, etc.) has different site boundaries
|
||||
# Would require: CREATE TABLE map_boundaries (map_name, site_name, min_x, max_x, min_y, max_y)
|
||||
# Commenting out these 3 features:
|
||||
# - int_pos_site_a_control_rate
|
||||
# - int_pos_site_b_control_rate
|
||||
# - int_pos_mid_control_rate
|
||||
|
||||
return {
|
||||
'int_pos_site_a_control_rate': 0.33, # Placeholder
|
||||
'int_pos_site_b_control_rate': 0.33, # Placeholder
|
||||
'int_pos_mid_control_rate': 0.34, # Placeholder
|
||||
'int_pos_favorite_position': 'mid',
|
||||
'int_pos_site_a_control_rate': None,
|
||||
'int_pos_site_b_control_rate': None,
|
||||
'int_pos_mid_control_rate': None,
|
||||
'int_pos_favorite_position': None,
|
||||
'int_pos_position_diversity': round(position_diversity, 3),
|
||||
'int_pos_rotation_speed': 50.0,
|
||||
'int_pos_rotation_speed': None,
|
||||
'int_pos_map_coverage': round(map_coverage, 3),
|
||||
'int_pos_lurk_tendency': 0.25,
|
||||
'int_pos_site_anchor_score': 50.0,
|
||||
'int_pos_lurk_tendency': None,
|
||||
'int_pos_site_anchor_score': None,
|
||||
'int_pos_entry_route_diversity': round(position_diversity, 3),
|
||||
'int_pos_retake_positioning': 50.0,
|
||||
'int_pos_postplant_positioning': 50.0,
|
||||
'int_pos_retake_positioning': None,
|
||||
'int_pos_postplant_positioning': None,
|
||||
'int_pos_spatial_iq_score': round(position_diversity * 100, 2),
|
||||
'int_pos_avg_distance_from_teammates': 500.0,
|
||||
'int_pos_avg_distance_from_teammates': None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -706,20 +694,20 @@ def _get_default_intelligence_features() -> Dict[str, Any]:
|
||||
'int_pressure_big_moment_score': 0.0,
|
||||
'int_pressure_tilt_resistance': 0.0,
|
||||
# Position Mastery (14)
|
||||
'int_pos_site_a_control_rate': 0.0,
|
||||
'int_pos_site_b_control_rate': 0.0,
|
||||
'int_pos_mid_control_rate': 0.0,
|
||||
'int_pos_favorite_position': 'unknown',
|
||||
'int_pos_position_diversity': 0.0,
|
||||
'int_pos_rotation_speed': 0.0,
|
||||
'int_pos_map_coverage': 0.0,
|
||||
'int_pos_lurk_tendency': 0.0,
|
||||
'int_pos_site_anchor_score': 0.0,
|
||||
'int_pos_entry_route_diversity': 0.0,
|
||||
'int_pos_retake_positioning': 0.0,
|
||||
'int_pos_postplant_positioning': 0.0,
|
||||
'int_pos_spatial_iq_score': 0.0,
|
||||
'int_pos_avg_distance_from_teammates': 0.0,
|
||||
'int_pos_site_a_control_rate': None,
|
||||
'int_pos_site_b_control_rate': None,
|
||||
'int_pos_mid_control_rate': None,
|
||||
'int_pos_favorite_position': None,
|
||||
'int_pos_position_diversity': None,
|
||||
'int_pos_rotation_speed': None,
|
||||
'int_pos_map_coverage': None,
|
||||
'int_pos_lurk_tendency': None,
|
||||
'int_pos_site_anchor_score': None,
|
||||
'int_pos_entry_route_diversity': None,
|
||||
'int_pos_retake_positioning': None,
|
||||
'int_pos_postplant_positioning': None,
|
||||
'int_pos_spatial_iq_score': None,
|
||||
'int_pos_avg_distance_from_teammates': None,
|
||||
# Trade Network (8)
|
||||
'int_trade_kill_count': 0,
|
||||
'int_trade_kill_rate': 0.0,
|
||||
|
||||
@@ -60,10 +60,11 @@ class MetaProcessor(BaseFeatureProcessor):
|
||||
|
||||
# Get recent matches for volatility
|
||||
cursor.execute("""
|
||||
SELECT rating
|
||||
FROM fact_match_players
|
||||
WHERE steam_id_64 = ?
|
||||
ORDER BY match_id DESC
|
||||
SELECT p.rating
|
||||
FROM fact_match_players p
|
||||
JOIN fact_matches m ON m.match_id = p.match_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
ORDER BY m.start_time DESC, p.match_id DESC
|
||||
LIMIT 20
|
||||
""", (steam_id,))
|
||||
|
||||
@@ -141,8 +142,35 @@ class MetaProcessor(BaseFeatureProcessor):
|
||||
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
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
CASE
|
||||
WHEN p.origin_elo - opponent.avg_elo > 200 THEN 'lower'
|
||||
WHEN p.origin_elo - opponent.avg_elo < -200 THEN 'higher'
|
||||
ELSE 'similar'
|
||||
END AS opponent_tier,
|
||||
AVG(p.rating) AS avg_rating
|
||||
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
|
||||
) opponent
|
||||
ON opponent.match_id = p.match_id
|
||||
AND opponent.team_id != p.team_id
|
||||
WHERE p.steam_id_64 = ?
|
||||
AND p.origin_elo IS NOT NULL
|
||||
AND p.rating IS NOT NULL
|
||||
GROUP BY opponent_tier
|
||||
""", (steam_id,))
|
||||
elo_tier_ratings = [
|
||||
row[1] for row in cursor.fetchall() if row[1] is not None
|
||||
]
|
||||
elo_tier_stability = SafeAggregator.safe_stddev(
|
||||
elo_tier_ratings,
|
||||
0.0,
|
||||
)
|
||||
|
||||
return {
|
||||
'meta_rating_volatility': round(rating_volatility, 3),
|
||||
|
||||
@@ -378,6 +378,56 @@ CREATE TABLE IF NOT EXISTS dm_player_weapon_stats (
|
||||
CREATE INDEX IF NOT EXISTS idx_player_weapon_stats_player ON dm_player_weapon_stats(steam_id_64);
|
||||
CREATE INDEX IF NOT EXISTS idx_player_weapon_stats_weapon ON dm_player_weapon_stats(weapon_name);
|
||||
|
||||
-- ============================================================================
|
||||
-- Profile Mart: Time-window statistics
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS dm_player_period_stats (
|
||||
steam_id_64 TEXT NOT NULL,
|
||||
period_key TEXT NOT NULL,
|
||||
period_label TEXT NOT NULL,
|
||||
period_start INTEGER,
|
||||
period_end INTEGER,
|
||||
matches INTEGER NOT NULL DEFAULT 0,
|
||||
wins INTEGER NOT NULL DEFAULT 0,
|
||||
win_rate REAL,
|
||||
avg_rating REAL,
|
||||
avg_kd REAL,
|
||||
avg_adr REAL,
|
||||
avg_kast REAL,
|
||||
total_kills INTEGER NOT NULL DEFAULT 0,
|
||||
total_deaths INTEGER NOT NULL DEFAULT 0,
|
||||
sample_reliable BOOLEAN NOT NULL DEFAULT 0,
|
||||
last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (steam_id_64, period_key),
|
||||
FOREIGN KEY (steam_id_64)
|
||||
REFERENCES dm_player_features(steam_id_64) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_player_period_player
|
||||
ON dm_player_period_stats(steam_id_64, period_key);
|
||||
|
||||
-- ============================================================================
|
||||
-- Profile Mart: Career records linked to the source match
|
||||
-- ============================================================================
|
||||
CREATE TABLE IF NOT EXISTS dm_player_records (
|
||||
steam_id_64 TEXT NOT NULL,
|
||||
record_key TEXT NOT NULL,
|
||||
record_label TEXT NOT NULL,
|
||||
record_value REAL,
|
||||
match_id TEXT,
|
||||
map_name TEXT,
|
||||
match_date INTEGER,
|
||||
last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (steam_id_64, record_key),
|
||||
FOREIGN KEY (steam_id_64)
|
||||
REFERENCES dm_player_features(steam_id_64) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_player_records_player
|
||||
ON dm_player_records(steam_id_64, record_key);
|
||||
|
||||
-- ============================================================================
|
||||
-- Schema Summary
|
||||
-- ============================================================================
|
||||
@@ -391,4 +441,6 @@ CREATE INDEX IF NOT EXISTS idx_player_weapon_stats_weapon ON dm_player_weapon_st
|
||||
-- dm_player_match_history: Per-match snapshots for trend analysis
|
||||
-- dm_player_map_stats: Map-level aggregations
|
||||
-- dm_player_weapon_stats: Weapon usage statistics
|
||||
-- dm_player_period_stats: Career/recent time-window aggregations
|
||||
-- dm_player_records: Career record values and source matches
|
||||
-- ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user