feat: timely_rating_filtrate #18

Merged
jacky merged 1 commits from timely_rating_filtrate into main 2026-01-28 15:17:33 +08:00
3 changed files with 131 additions and 1 deletions
Showing only changes of commit f110ae52f0 - Show all commits

View File

@@ -202,6 +202,9 @@ def detail(steam_id):
})
map_stats_list.sort(key=lambda x: x['matches'], reverse=True)
# --- New: Recent Performance Stats ---
recent_stats = StatsService.get_recent_performance_stats(steam_id)
return render_template('players/profile.html',
player=player,
features=features,
@@ -211,7 +214,8 @@ def detail(steam_id):
distribution=distribution,
map_stats=map_stats_list,
l2_stats=l2_stats,
side_stats=side_stats)
side_stats=side_stats,
recent_stats=recent_stats)
@bp.route('/comment/<int:comment_id>/like', methods=['POST'])
def like_comment(comment_id):

View File

@@ -632,6 +632,72 @@ class StatsService:
"""
return query_db('l2', sql, [steam_id, limit])
@staticmethod
def get_recent_performance_stats(steam_id):
"""
Calculates Avg Rating and Rating Variance for:
- Last 5, 10, 15 matches
- Last 5, 10, 15 days
"""
import numpy as np
from datetime import datetime, timedelta
# Fetch all match ratings with timestamps
sql = """
SELECT m.start_time, mp.rating
FROM fact_match_players mp
JOIN fact_matches m ON mp.match_id = m.match_id
WHERE mp.steam_id_64 = ?
ORDER BY m.start_time DESC
"""
rows = query_db('l2', sql, [steam_id])
if not rows:
return {}
# Convert to list of dicts
matches = [{'time': r['start_time'], 'rating': r['rating'] or 0} for r in rows]
stats = {}
# 1. Recent N Matches
for n in [5, 10, 15]:
subset = matches[:n]
if not subset:
stats[f'last_{n}_matches'] = {'avg': 0, 'var': 0, 'count': 0}
continue
ratings = [m['rating'] for m in subset]
stats[f'last_{n}_matches'] = {
'avg': np.mean(ratings),
'var': np.var(ratings),
'count': len(ratings)
}
# 2. Recent N Days
# Use server time or max match time? usually server time 'now' is fine if data is fresh.
# But if data is old, 'last 5 days' might be empty.
# User asked for "recent 5/10/15 days", implying calendar days from now.
import time
now = time.time()
for d in [5, 10, 15]:
cutoff = now - (d * 24 * 3600)
subset = [m for m in matches if m['time'] >= cutoff]
if not subset:
stats[f'last_{d}_days'] = {'avg': 0, 'var': 0, 'count': 0}
continue
ratings = [m['rating'] for m in subset]
stats[f'last_{d}_days'] = {
'avg': np.mean(ratings),
'var': np.var(ratings),
'count': len(ratings)
}
return stats
@staticmethod
def get_roster_stats_distribution(target_steam_id):
"""

View File

@@ -141,6 +141,66 @@
</div>
</div>
<!-- 2.2 Recent Performance Stability -->
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-2xl p-6 border border-gray-100 dark:border-slate-700">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
<span>📅</span> 近期表现稳定性 (Recent Stability)
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- By Match Count -->
<div>
<h4 class="text-sm font-bold text-gray-500 uppercase tracking-wider mb-4">By Matches</h4>
<div class="space-y-4">
{% for n in [5, 10, 15] %}
{% set key = 'last_' ~ n ~ '_matches' %}
{% set data = recent_stats.get(key) %}
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-slate-700/50 rounded-lg border border-gray-100 dark:border-slate-600">
<div class="flex items-center gap-3">
<span class="text-2xl font-black text-gray-300 dark:text-slate-600 w-8 text-center">{{ n }}</span>
<span class="text-xs font-bold text-gray-500 uppercase">Matches</span>
</div>
<div class="text-right">
{% if data and data.count > 0 %}
<div class="text-xl font-black text-gray-900 dark:text-white">{{ "{:.2f}".format(data.avg) }} <span class="text-xs text-gray-400 font-normal">Rating</span></div>
<div class="text-xs text-gray-500 font-mono">Var: {{ "{:.3f}".format(data.var) }}</div>
{% else %}
<span class="text-xs text-gray-400">N/A</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- By Days -->
<div>
<h4 class="text-sm font-bold text-gray-500 uppercase tracking-wider mb-4">By Days</h4>
<div class="space-y-4">
{% for n in [5, 10, 15] %}
{% set key = 'last_' ~ n ~ '_days' %}
{% set data = recent_stats.get(key) %}
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-slate-700/50 rounded-lg border border-gray-100 dark:border-slate-600">
<div class="flex items-center gap-3">
<span class="text-2xl font-black text-gray-300 dark:text-slate-600 w-8 text-center">{{ n }}</span>
<span class="text-xs font-bold text-gray-500 uppercase">Days</span>
</div>
<div class="text-right">
{% if data and data.count > 0 %}
<div class="text-xl font-black text-gray-900 dark:text-white">{{ "{:.2f}".format(data.avg) }} <span class="text-xs text-gray-400 font-normal">Rating</span></div>
<div class="text-xs text-gray-500 font-mono">Var: {{ "{:.3f}".format(data.var) }}</div>
<div class="text-[10px] text-gray-400">{{ data.count }} matches</div>
{% else %}
<span class="text-xs text-gray-400">No matches</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- 2.5 Detailed Stats Panel -->
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-2xl p-6 border border-gray-100 dark:border-slate-700">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">