feat: timely_rating_filtrate #18
@@ -202,6 +202,9 @@ def detail(steam_id):
|
|||||||
})
|
})
|
||||||
map_stats_list.sort(key=lambda x: x['matches'], reverse=True)
|
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',
|
return render_template('players/profile.html',
|
||||||
player=player,
|
player=player,
|
||||||
features=features,
|
features=features,
|
||||||
@@ -211,7 +214,8 @@ def detail(steam_id):
|
|||||||
distribution=distribution,
|
distribution=distribution,
|
||||||
map_stats=map_stats_list,
|
map_stats=map_stats_list,
|
||||||
l2_stats=l2_stats,
|
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'])
|
@bp.route('/comment/<int:comment_id>/like', methods=['POST'])
|
||||||
def like_comment(comment_id):
|
def like_comment(comment_id):
|
||||||
|
|||||||
@@ -632,6 +632,72 @@ class StatsService:
|
|||||||
"""
|
"""
|
||||||
return query_db('l2', sql, [steam_id, limit])
|
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
|
@staticmethod
|
||||||
def get_roster_stats_distribution(target_steam_id):
|
def get_roster_stats_distribution(target_steam_id):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -141,6 +141,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- 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">
|
<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">
|
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user