2.0.0 Alpha: Data Refinery
This commit is contained in:
+102
-163
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user