2.0.0 Alpha: Data Refinery

This commit is contained in:
2026-08-08 21:31:56 +08:00
parent fa75081d4d
commit 562775e5db
48 changed files with 4172 additions and 661 deletions
+102 -163
View File
@@ -1,4 +1,4 @@
from web.database import query_db, execute_db
from web.database import query_db
from flask import current_app, url_for
import os
@@ -13,7 +13,7 @@ class StatsService:
try:
# Check local file first (User Request: "directly associate if exists")
base = os.path.join(current_app.root_path, 'static', 'avatars')
for ext in ('.jpg', '.png', '.jpeg'):
for ext in ('.jpg', '.png', '.jpeg', '.webp'):
fname = f"{steam_id}{ext}"
fpath = os.path.join(base, fname)
if os.path.exists(fpath):
@@ -38,18 +38,9 @@ class StatsService:
'round_stats': [{'type', 'count', 'wins', 'win_rate'}]
}
"""
# 1. Get Active Roster
from web.services.web_service import WebService
import json
lineups = WebService.get_lineups()
active_roster_ids = []
if lineups:
try:
raw_ids = json.loads(lineups[0]['player_ids_json'])
active_roster_ids = [str(uid) for uid in raw_ids]
except:
pass
from web.services.team_context_service import TeamContextService
active_roster_ids = TeamContextService.get_active_roster_ids()
if not active_roster_ids:
return {}
@@ -60,21 +51,23 @@ class StatsService:
placeholders = ','.join('?' for _ in active_roster_ids)
# Step A: Get Candidate Match IDs (matches with >= 2 roster players)
# Also get the team_id of our players in that match to determine win
candidate_sql = f"""
SELECT mp.match_id, MAX(mp.team_id) as our_team_id
SELECT mp.match_id, mp.team_id as our_team_id,
COUNT(DISTINCT mp.steam_id_64) as roster_count
FROM fact_match_players mp
WHERE CAST(mp.steam_id_64 AS TEXT) IN ({placeholders})
GROUP BY mp.match_id
GROUP BY mp.match_id, mp.team_id
HAVING COUNT(DISTINCT mp.steam_id_64) >= 2
ORDER BY mp.match_id, roster_count DESC, mp.team_id
"""
candidate_rows = query_db('l2', candidate_sql, active_roster_ids)
if not candidate_rows:
return {}
candidate_map = {row['match_id']: row['our_team_id'] for row in candidate_rows}
candidate_map = {}
for row in candidate_rows:
candidate_map.setdefault(row['match_id'], row['our_team_id'])
match_ids = list(candidate_map.keys())
match_placeholders = ','.join('?' for _ in match_ids)
@@ -221,11 +214,15 @@ class StatsService:
args.append(map_name)
if date_from:
where_clauses.append("start_time >= ?")
where_clauses.append(
"start_time >= CAST(strftime('%s', ?) AS INTEGER)"
)
args.append(date_from)
if date_to:
where_clauses.append("start_time <= ?")
where_clauses.append(
"start_time < CAST(strftime('%s', date(?, '+1 day')) AS INTEGER)"
)
args.append(date_to)
where_str = " AND ".join(where_clauses)
@@ -270,109 +267,51 @@ class StatsService:
party_rows = query_db('l2', party_sql, match_ids)
party_map = {row['match_id']: row['max_party'] for row in party_rows}
# --- New: Determine "Our Team" Result ---
# Logic: Check if any player from `active_roster` played in these matches.
# Use WebService to get the active roster
from web.services.web_service import WebService
import json
lineups = WebService.get_lineups()
active_roster_ids = []
if lineups:
try:
# Load IDs and ensure they are all strings for DB comparison consistency
raw_ids = json.loads(lineups[0]['player_ids_json'])
active_roster_ids = [str(uid) for uid in raw_ids]
except:
pass
from web.services.team_context_service import TeamContextService
active_roster_ids = TeamContextService.get_active_roster_ids()
# If no roster, we can't determine "Our Result"
if not active_roster_ids:
result_map = {}
else:
# 1. Get UIDs for Roster Members involved in these matches
# We query fact_match_players to ensure we get the UIDs actually used in these matches
roster_placeholders = ','.join('?' for _ in active_roster_ids)
uid_sql = f"""
SELECT DISTINCT steam_id_64, uid
roster_team_sql = f"""
SELECT match_id, team_id,
COUNT(DISTINCT steam_id_64) as roster_count
FROM fact_match_players
WHERE match_id IN ({placeholders})
AND CAST(steam_id_64 AS TEXT) IN ({roster_placeholders})
GROUP BY match_id, team_id
"""
combined_args_uid = match_ids + active_roster_ids
uid_rows = query_db('l2', uid_sql, combined_args_uid)
# Set of "Our UIDs" (as strings)
our_uids = set()
for r in uid_rows:
if r['uid']:
our_uids.add(str(r['uid']))
# 2. Get Group UIDs and Winner info from fact_match_teams
# We need to know which group contains our UIDs
teams_sql = f"""
SELECT fmt.match_id, fmt.group_id, fmt.group_uids, m.winner_team
FROM fact_match_teams fmt
JOIN fact_matches m ON fmt.match_id = m.match_id
WHERE fmt.match_id IN ({placeholders})
"""
teams_rows = query_db('l2', teams_sql, match_ids)
# 3. Determine Result per Match
roster_team_rows = query_db(
'l2',
roster_team_sql,
match_ids + active_roster_ids,
)
winner_by_match = {
str(match['match_id']): match['winner_team']
for match in matches
}
teams_by_match = {}
for row in roster_team_rows:
teams_by_match.setdefault(str(row['match_id']), []).append(
row['team_id']
)
result_map = {}
# Group data by match
match_groups = {} # match_id -> {group_id: [uids...], winner: int}
for r in teams_rows:
mid = r['match_id']
gid = r['group_id']
uids_str = r['group_uids'] or ""
# Split and clean UIDs
uids = set(str(u).strip() for u in uids_str.split(',') if u.strip())
if mid not in match_groups:
match_groups[mid] = {'groups': {}, 'winner': r['winner_team']}
match_groups[mid]['groups'][gid] = uids
# Analyze
for mid, data in match_groups.items():
winner_gid = data['winner']
groups = data['groups']
our_in_winner = False
our_in_loser = False
# Check each group
for gid, uids in groups.items():
# Intersection of Our UIDs and Group UIDs
common = our_uids.intersection(uids)
if common:
if gid == winner_gid:
our_in_winner = True
else:
our_in_loser = True
if our_in_winner and not our_in_loser:
result_map[mid] = 'win'
elif our_in_loser and not our_in_winner:
result_map[mid] = 'loss'
elif our_in_winner and our_in_loser:
result_map[mid] = 'mixed'
else:
# Fallback: If UID matching failed (maybe missing UIDs), try old team_id method?
# Or just leave it as None (safe)
pass
for match_id, team_ids in teams_by_match.items():
unique_team_ids = set(team_ids)
if len(unique_team_ids) > 1:
result_map[match_id] = 'mixed'
continue
our_team_id = next(iter(unique_team_ids))
result_map[match_id] = (
'win'
if str(our_team_id) == str(winner_by_match.get(match_id))
else 'loss'
)
# Convert to dict to modify
matches = [dict(m) for m in matches]
for m in matches:
m['avg_elo'] = elo_map.get(m['match_id'], 0)
m['max_party'] = party_map.get(m['match_id'], 1)
m['our_result'] = result_map.get(m['match_id'])
# Convert to dict to modify
matches = [dict(m) for m in matches]
for m in matches:
m['avg_elo'] = elo_map.get(m['match_id'], 0)
@@ -542,33 +481,20 @@ class StatsService:
@staticmethod
def get_shared_matches(steam_ids):
# Find matches where ALL steam_ids were present
if not steam_ids or len(steam_ids) < 1:
return []
steam_ids = list(dict.fromkeys(str(steam_id) for steam_id in steam_ids))
placeholders = ','.join('?' for _ in steam_ids)
count = len(steam_ids)
# We need to know which team the players were on to determine win/loss
# Assuming they were on the SAME team for "shared experience"
# If count=1, it's just match history
# Query: Get matches where all steam_ids are present
# Also join to get team_id to check if they were on the same team (optional but better)
# For simplicity in v1: Just check presence in the match.
# AND check if the player won.
# We need to return: match_id, map_name, score, result (Win/Loss)
# "Result" is relative to the lineup.
# If they were on the winning team, it's a Win.
sql = f"""
SELECT m.match_id, m.start_time, m.map_name, m.score_team1, m.score_team2, m.winner_team,
MAX(mp.team_id) as player_team_id -- Just take one team_id (assuming same)
mp.team_id as player_team_id
FROM fact_matches m
JOIN fact_match_players mp ON m.match_id = mp.match_id
WHERE mp.steam_id_64 IN ({placeholders})
GROUP BY m.match_id
GROUP BY m.match_id, mp.team_id
HAVING COUNT(DISTINCT mp.steam_id_64) = ?
ORDER BY m.start_time DESC
"""
@@ -580,14 +506,7 @@ class StatsService:
results = []
for r in rows:
# Determine if Win
# winner_team in DB is 'Team 1' or 'Team 2' usually, or the team name.
# fact_matches.winner_team stores the NAME of the winner? Or 'team1'/'team2'?
# Let's check how L2_Builder stores it. Usually it stores the name.
# But fact_match_players.team_id stores the name too.
# Logic: If m.winner_team == mp.team_id, then Win.
is_win = (r['winner_team'] == r['player_team_id'])
is_win = str(r['winner_team']) == str(r['player_team_id'])
# If winner_team is NULL or empty, it's a draw?
if not r['winner_team']:
@@ -628,7 +547,31 @@ class StatsService:
"""
l3_rows = query_db("l3", l3_sql, [steam_id, limit])
if l3_rows:
return l3_rows
history = [dict(row) for row in l3_rows]
match_ids = [row['match_id'] for row in history]
placeholders = ','.join('?' for _ in match_ids)
party_rows = query_db(
"l2",
f"""
SELECT me.match_id, COUNT(p.steam_id_64) AS party_size
FROM fact_match_players me
LEFT JOIN fact_match_players p
ON p.match_id = me.match_id
AND p.match_team_id = me.match_team_id
AND me.match_team_id > 0
WHERE me.steam_id_64 = ?
AND me.match_id IN ({placeholders})
GROUP BY me.match_id
""",
[steam_id] + match_ids,
)
party_map = {
row['match_id']: max(int(row['party_size'] or 0), 1)
for row in party_rows
}
for row in history:
row['party_size'] = party_map.get(row['match_id'], 1)
return history
sql = """
SELECT * FROM (
@@ -729,19 +672,10 @@ class StatsService:
Calculates rank and distribution of the target player within the active roster.
Now covers all L3 Basic Features for Detailed Panel.
"""
from web.services.web_service import WebService
from web.services.feature_service import FeatureService
import json
# 1. Get Active Roster IDs
lineups = WebService.get_lineups()
active_roster_ids = []
if lineups:
try:
raw_ids = json.loads(lineups[0]['player_ids_json'])
active_roster_ids = [str(uid) for uid in raw_ids]
except:
pass
from web.services.team_context_service import TeamContextService
active_roster_ids = TeamContextService.get_active_roster_ids()
if not active_roster_ids:
return None
@@ -851,33 +785,38 @@ class StatsService:
"basic_avg_rating", "basic_avg_kd", "basic_avg_adr", "basic_avg_kast", "basic_avg_rws",
]
lower_is_better = []
lower_is_better = {
"int_timing_first_contact_time",
"int_trade_response_time",
"tac_avg_fd",
"tac_fd_rate",
"core_avg_match_duration",
"core_dpr",
"meta_rating_volatility",
"meta_map_stability",
"meta_elo_tier_stability",
}
result = {}
for m in metrics:
values = []
non_numeric = False
for p in stats_map.values():
raw = (p or {}).get(m)
if raw is None:
raw = 0
continue
try:
values.append(float(raw))
except Exception:
non_numeric = True
break
except (TypeError, ValueError):
continue
raw_target = (stats_map.get(target_steam_id) or {}).get(m)
if raw_target is None:
raw_target = 0
result[m] = None
continue
try:
target_val = float(raw_target)
except Exception:
non_numeric = True
target_val = 0
if non_numeric:
except (TypeError, ValueError):
result[m] = None
continue