Support Specialist v1: per-map league metrics + normalized stats
This commit is contained in:
parent
3dc737d819
commit
ef88da9c53
13 changed files with 438 additions and 65 deletions
|
|
@ -5,6 +5,12 @@ from analysis.team_identity_v2 import format_team_identity, generate_team_identi
|
||||||
from analysis.storylines import generate_storyline
|
from analysis.storylines import generate_storyline
|
||||||
from analysis.predictions_v2 import generate_prediction
|
from analysis.predictions_v2 import generate_prediction
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
def safe_filename(name):
|
||||||
|
# Remove all illegal Windows filename characters
|
||||||
|
return re.sub(r'[\\/:*?"<>|]', '', name)
|
||||||
|
|
||||||
|
|
||||||
def export_obs_player_summaries(output_folder, team_players):
|
def export_obs_player_summaries(output_folder, team_players):
|
||||||
"""
|
"""
|
||||||
|
|
@ -15,7 +21,7 @@ def export_obs_player_summaries(output_folder, team_players):
|
||||||
|
|
||||||
for p in team_players:
|
for p in team_players:
|
||||||
name = p.get("name", "Unknown")
|
name = p.get("name", "Unknown")
|
||||||
safe_name = name.replace(" ", "_")
|
safe_name = safe_filename(name)
|
||||||
|
|
||||||
path = os.path.join(output_folder, f"{safe_name}_summary.txt")
|
path = os.path.join(output_folder, f"{safe_name}_summary.txt")
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
|
@ -29,7 +35,8 @@ def export_obs_team_identity(output_folder, team_players, team_name):
|
||||||
|
|
||||||
os.makedirs(output_folder, exist_ok=True)
|
os.makedirs(output_folder, exist_ok=True)
|
||||||
|
|
||||||
path = os.path.join(output_folder, f"{team_name}_identity.txt")
|
safe_team = safe_filename(team_name)
|
||||||
|
path = os.path.join(output_folder, f"{safe_team}_identity.txt")
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
identity_data = generate_team_identity_block(team_players, team_name)
|
identity_data = generate_team_identity_block(team_players, team_name)
|
||||||
identity_text = format_team_identity(identity_data)
|
identity_text = format_team_identity(identity_data)
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,18 @@ def build_player_summary(player):
|
||||||
- Sharpshooter
|
- Sharpshooter
|
||||||
- Consistency
|
- Consistency
|
||||||
- Clutch
|
- Clutch
|
||||||
|
- Support Specialist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def safe_pct(value):
|
||||||
|
try:
|
||||||
|
return float(value) if value is not None else 0.0
|
||||||
|
except:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def safe_tier(value):
|
||||||
|
return value if value not in (None, "", "None") else "D"
|
||||||
|
|
||||||
name = player.get("name", "Unknown Player")
|
name = player.get("name", "Unknown Player")
|
||||||
|
|
||||||
slayer = player.get("slayer", {})
|
slayer = player.get("slayer", {})
|
||||||
|
|
@ -16,28 +26,33 @@ def build_player_summary(player):
|
||||||
sharp = player.get("sharpshooter", {})
|
sharp = player.get("sharpshooter", {})
|
||||||
consistency = player.get("consistency", {})
|
consistency = player.get("consistency", {})
|
||||||
clutch = player.get("clutch", {})
|
clutch = player.get("clutch", {})
|
||||||
|
support = player.get("support_specialist", {})
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
lines.append(f"{name}\n")
|
lines.append(f"{name}\n")
|
||||||
|
|
||||||
# Slayer
|
# Slayer
|
||||||
lines.append(f"Slayer: {slayer.get('tier', 'D')} ({slayer.get('pct', 0):.1f}%)")
|
lines.append(f"Slayer: {safe_tier(slayer.get('tier'))} ({safe_pct(slayer.get('pct')):.1f}%)")
|
||||||
lines.append(f" {slayer.get('summary', '')}")
|
lines.append(f" {slayer.get('summary', '')}")
|
||||||
|
|
||||||
# Payload
|
# Payload
|
||||||
lines.append(f"\nPayload Objective: {payload.get('tier', 'D')} ({payload.get('pct', 0):.1f}%)")
|
lines.append(f"\nPayload Objective: {safe_tier(payload.get('tier'))} ({safe_pct(payload.get('pct')):.1f}%)")
|
||||||
lines.append(f" {payload.get('summary', '')}")
|
lines.append(f" {payload.get('summary', '')}")
|
||||||
|
|
||||||
# Sharpshooter
|
# Sharpshooter
|
||||||
lines.append(f"\nSharpshooter: {sharp.get('tier', 'D')} ({sharp.get('pct', 0):.1f}%)")
|
lines.append(f"\nSharpshooter: {safe_tier(sharp.get('tier'))} ({safe_pct(sharp.get('pct')):.1f}%)")
|
||||||
lines.append(f" {sharp.get('summary', '')}")
|
lines.append(f" {sharp.get('summary', '')}")
|
||||||
|
|
||||||
# Consistency
|
# Consistency
|
||||||
lines.append(f"\nConsistency: {consistency.get('tier', 'D')} ({consistency.get('pct', 0):.1f}%)")
|
lines.append(f"\nConsistency: {safe_tier(consistency.get('tier'))} ({safe_pct(consistency.get('pct')):.1f}%)")
|
||||||
lines.append(f" {consistency.get('summary', '')}")
|
lines.append(f" {consistency.get('summary', '')}")
|
||||||
|
|
||||||
# Clutch
|
# Clutch
|
||||||
lines.append(f"\nClutch: {clutch.get('tier', 'D')} ({clutch.get('pct', 0):.1f}%)")
|
lines.append(f"\nClutch: {safe_tier(clutch.get('tier'))} ({safe_pct(clutch.get('pct')):.1f}%)")
|
||||||
lines.append(f" {clutch.get('summary', '')}")
|
lines.append(f" {clutch.get('summary', '')}")
|
||||||
|
|
||||||
|
# Support Specialist
|
||||||
|
lines.append(f"\nSupport Specialist: {safe_tier(support.get('tier'))} ({safe_pct(support.get('pct')):.1f}%)")
|
||||||
|
lines.append(f" {support.get('summary', '')}")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
@ -1,23 +1,16 @@
|
||||||
|
|
||||||
|
|
||||||
from analysis.team_identity_v2 import summarize_team_tags, classify_team_style
|
from analysis.team_identity_v2 import summarize_team_tags, classify_team_style
|
||||||
|
from utils.safe_tag import safe_pct, safe_avg, safe_tier
|
||||||
|
|
||||||
|
|
||||||
def compute_team_power_score(summary):
|
def compute_team_power_score(summary):
|
||||||
"""
|
|
||||||
Converts team tag percentiles into a single weighted power score.
|
|
||||||
Weights reflect real match impact:
|
|
||||||
Slayer: 30%
|
|
||||||
Objective: 25%
|
|
||||||
Consistency: 20%
|
|
||||||
Clutch: 15%
|
|
||||||
Sharpshooter: 10%
|
|
||||||
"""
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
summary["slayer"]["avg_pct"] * 0.30 +
|
safe_pct(summary["slayer"]["avg_pct"]) * 0.30 +
|
||||||
summary["objective_payload"]["avg_pct"] * 0.25 +
|
safe_pct(summary["objective_payload"]["avg_pct"]) * 0.25 +
|
||||||
summary["consistency"]["avg_pct"] * 0.20 +
|
safe_pct(summary["consistency"]["avg_pct"]) * 0.20 +
|
||||||
summary["clutch"]["avg_pct"] * 0.15 +
|
safe_pct(summary["clutch"]["avg_pct"]) * 0.15 +
|
||||||
summary["sharpshooter"]["avg_pct"] * 0.10
|
safe_pct(summary["sharpshooter"]["avg_pct"]) * 0.10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ __all__ = ["matchup_storyline", "generate_storyline"]
|
||||||
# Team Tag Aggregation
|
# Team Tag Aggregation
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
from utils.safe_tag import safe_pct, safe_tier, safe_avg
|
||||||
|
|
||||||
def summarize_team_tags(team_players):
|
def summarize_team_tags(team_players):
|
||||||
"""
|
"""
|
||||||
Aggregates tag tiers + percentiles for a team.
|
Aggregates tag tiers + percentiles for a team.
|
||||||
|
|
@ -32,23 +34,11 @@ def summarize_team_tags(team_players):
|
||||||
|
|
||||||
for p in team_players:
|
for p in team_players:
|
||||||
tag_data = p.get(tag, {})
|
tag_data = p.get(tag, {})
|
||||||
pcts.append(tag_data.get("pct", 0))
|
pcts.append(safe_pct(tag_data.get("pct")))
|
||||||
tiers.append(tag_data.get("tier", "D"))
|
tiers.append(safe_tier(tag_data.get("tier")))
|
||||||
|
|
||||||
if not pcts:
|
|
||||||
summary[tag] = {
|
|
||||||
"avg_pct": 0,
|
|
||||||
"top_tier": "D",
|
|
||||||
"count_S": 0,
|
|
||||||
"count_A": 0,
|
|
||||||
"count_B": 0,
|
|
||||||
"count_C": 0,
|
|
||||||
"count_D": 0,
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
|
|
||||||
summary[tag] = {
|
summary[tag] = {
|
||||||
"avg_pct": sum(p for p in pcts if isinstance(p, (int, float))) / max(1, len([p for p in pcts if isinstance(p, (int, float))])),
|
"avg_pct": safe_avg(pcts),
|
||||||
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
||||||
"count_S": tiers.count("S"),
|
"count_S": tiers.count("S"),
|
||||||
"count_A": tiers.count("A"),
|
"count_A": tiers.count("A"),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
|
|
||||||
|
from utils.safe_tag import safe_pct, safe_tier, safe_avg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from utils.safe_tag import safe_pct, safe_tier, safe_avg
|
||||||
|
|
||||||
def summarize_team_tags(team_players):
|
def summarize_team_tags(team_players):
|
||||||
"""
|
"""
|
||||||
Aggregates tag tiers + percentiles for a team.
|
Aggregates tag tiers + percentiles for a team.
|
||||||
|
|
@ -16,23 +23,11 @@ def summarize_team_tags(team_players):
|
||||||
|
|
||||||
for p in team_players:
|
for p in team_players:
|
||||||
tag_data = p.get(tag, {})
|
tag_data = p.get(tag, {})
|
||||||
pcts.append(tag_data.get("pct", 0))
|
pcts.append(safe_pct(tag_data.get("pct")))
|
||||||
tiers.append(tag_data.get("tier", "D"))
|
tiers.append(safe_tier(tag_data.get("tier")))
|
||||||
|
|
||||||
if not pcts:
|
|
||||||
summary[tag] = {
|
|
||||||
"avg_pct": 0,
|
|
||||||
"top_tier": "D",
|
|
||||||
"count_S": 0,
|
|
||||||
"count_A": 0,
|
|
||||||
"count_B": 0,
|
|
||||||
"count_C": 0,
|
|
||||||
"count_D": 0,
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
|
|
||||||
summary[tag] = {
|
summary[tag] = {
|
||||||
"avg_pct": sum(p for p in pcts if isinstance(p, (int, float))) / max(1, len([p for p in pcts if isinstance(p, (int, float))])),
|
"avg_pct": safe_avg(pcts),
|
||||||
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
||||||
"count_S": tiers.count("S"),
|
"count_S": tiers.count("S"),
|
||||||
"count_A": tiers.count("A"),
|
"count_A": tiers.count("A"),
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ from match_engine import (
|
||||||
split_two_columns,
|
split_two_columns,
|
||||||
compute_slayer_prediction,
|
compute_slayer_prediction,
|
||||||
rank_match_players_consistency_career,
|
rank_match_players_consistency_career,
|
||||||
|
rank_match_players_support_specialist,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
@ -377,14 +378,14 @@ class DashLeagueGUI:
|
||||||
p["objective_payload"] = pdata.get("objective_payload", {})
|
p["objective_payload"] = pdata.get("objective_payload", {})
|
||||||
|
|
||||||
# DO NOT overwrite match-based DomObj
|
# DO NOT overwrite match-based DomObj
|
||||||
if "objective_domination_components" not in p:
|
if "objective_domination" not in p:
|
||||||
p["objective_domination_components"] = pdata.get(
|
p["objective_domination"] = pdata.get("objective_domination", {})
|
||||||
"objective_domination_components",
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
p["consistency"] = pdata.get("consistency", {})
|
p["consistency"] = pdata.get("consistency", {})
|
||||||
p["clutch"] = pdata.get("clutch", {})
|
p["clutch"] = pdata.get("clutch", {})
|
||||||
|
|
||||||
|
# Support Specialist
|
||||||
|
p["support_specialist"] = pdata.get("support_specialist", {})
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
|
|
@ -591,6 +592,15 @@ Consistency Summary:
|
||||||
if isinstance(score, (int, float)):
|
if isinstance(score, (int, float)):
|
||||||
tags_out.append(f"Cons {score:.2f}")
|
tags_out.append(f"Cons {score:.2f}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Support Specialist (career)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
support = player_entry.get("support_specialist", {})
|
||||||
|
if isinstance(support, dict):
|
||||||
|
score = support.get("raw")
|
||||||
|
if isinstance(score, (int, float)):
|
||||||
|
tags_out.append(f"Supp {score:.2f}")
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Payload Objective Specialist (match-based)
|
# Payload Objective Specialist (match-based)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
@ -708,6 +718,7 @@ Consistency Summary:
|
||||||
"Domination Objective Specialist",
|
"Domination Objective Specialist",
|
||||||
"Consistency",
|
"Consistency",
|
||||||
"Clutch",
|
"Clutch",
|
||||||
|
"Support Specialist",
|
||||||
]
|
]
|
||||||
|
|
||||||
ttk.Label(win, text="Select Tag:").pack(anchor="w", padx=10, pady=5)
|
ttk.Label(win, text="Select Tag:").pack(anchor="w", padx=10, pady=5)
|
||||||
|
|
@ -744,7 +755,9 @@ Consistency Summary:
|
||||||
elif tag == "Consistency":
|
elif tag == "Consistency":
|
||||||
ranked = rank_match_players_consistency_career(self.current_match_players)
|
ranked = rank_match_players_consistency_career(self.current_match_players)
|
||||||
|
|
||||||
|
elif tag == "Support Specialist":
|
||||||
|
ranked = rank_match_players_support_specialist(self.current_match_players)
|
||||||
|
|
||||||
else: # Sharpshooter
|
else: # Sharpshooter
|
||||||
ranked = rank_match_players_sharpshooter(self.current_match_players)
|
ranked = rank_match_players_sharpshooter(self.current_match_players)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ from tags.objective_domination import compute_dom_objective_for_career_player
|
||||||
from tags.sharpshooter import compute_sharpshooter_for_career_player
|
from tags.sharpshooter import compute_sharpshooter_for_career_player
|
||||||
from tags.consistency import build_consistency_tag
|
from tags.consistency import build_consistency_tag
|
||||||
from tags.clutch import compute_clutch, compute_clutch_raw
|
from tags.clutch import compute_clutch, compute_clutch_raw
|
||||||
|
from tags.support_specialist import build_support_specialist_tag
|
||||||
|
|
||||||
from data.league_metrics import compute_league_metrics, compute_league_clutch_metrics
|
from data.league_metrics import compute_league_metrics, compute_league_clutch_metrics
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def build_career_database(output_path):
|
def build_career_database(output_path):
|
||||||
|
|
||||||
print("USING DB:", db_access.DB_PATH)
|
print("USING DB:", db_access.DB_PATH)
|
||||||
|
|
@ -101,6 +103,66 @@ def build_career_database(output_path):
|
||||||
|
|
||||||
for pdata in all_players:
|
for pdata in all_players:
|
||||||
pdata["clutch_raw"] = compute_clutch_raw(pdata)
|
pdata["clutch_raw"] = compute_clutch_raw(pdata)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# NORMALIZE CAREER STAT KEYS BEFORE LEAGUE METRICS
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
for pid, pdata in final_db["players"].items():
|
||||||
|
career = pdata.get("career", {})
|
||||||
|
|
||||||
|
career["push_time"] = (
|
||||||
|
career.get("push_time")
|
||||||
|
or career.get("PAY_PushTime")
|
||||||
|
or pdata.get("PAY_PushTime")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
career["captures"] = (
|
||||||
|
career.get("captures")
|
||||||
|
or career.get("DOM_Captures")
|
||||||
|
or pdata.get("DOM_Captures")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
career["counters"] = (
|
||||||
|
career.get("counters")
|
||||||
|
or career.get("DOM_Counters")
|
||||||
|
or pdata.get("DOM_Counters")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
career["damage"] = (
|
||||||
|
career.get("damage")
|
||||||
|
or career.get("Damage")
|
||||||
|
or pdata.get("Damage")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
career["kills"] = (
|
||||||
|
career.get("kills")
|
||||||
|
or career.get("Kills")
|
||||||
|
or pdata.get("Kills")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
career["deaths"] = (
|
||||||
|
career.get("deaths")
|
||||||
|
or career.get("Deaths")
|
||||||
|
or pdata.get("Deaths")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
career["maps"] = (
|
||||||
|
career.get("maps")
|
||||||
|
or pdata.get("maps")
|
||||||
|
or pdata.get("career", {}).get("maps")
|
||||||
|
or 1
|
||||||
|
)
|
||||||
|
|
||||||
|
pdata["career"] = career
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 4. Compute league metrics for ALL TAGS
|
# 4. Compute league metrics for ALL TAGS
|
||||||
|
|
@ -112,7 +174,22 @@ def build_career_database(output_path):
|
||||||
league_averages.update(clutch_averages)
|
league_averages.update(clutch_averages)
|
||||||
|
|
||||||
final_db["league_averages"] = league_averages
|
final_db["league_averages"] = league_averages
|
||||||
|
|
||||||
|
|
||||||
|
from tags.support_specialist import compute_support_specialist # at top of file if not already
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 4.5 Build Support Specialist distribution
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
support_distribution = []
|
||||||
|
for pid, pdata in final_db["players"].items():
|
||||||
|
raw_support = compute_support_specialist(pdata, league_averages)
|
||||||
|
support_distribution.append(raw_support)
|
||||||
|
|
||||||
|
# Write back normalized career block
|
||||||
|
pdata["career"] = career
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 5. Compute all tags using distributions
|
# 5. Compute all tags using distributions
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
@ -140,12 +217,45 @@ def build_career_database(output_path):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Domination Objective Specialist
|
# Domination Objective Specialist
|
||||||
pdata["objective_domination_components"] = compute_dom_objective_for_career_player(
|
pdata["objective_domination"] = compute_dom_objective_for_career_player(
|
||||||
pdata,
|
pdata,
|
||||||
league_averages,
|
league_averages,
|
||||||
distributions["domination"]
|
distributions["domination"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Consistency (legacy adapter)
|
||||||
|
consistency_result = build_consistency_tag(
|
||||||
|
floor_current=pdata.get("floor_current"),
|
||||||
|
floor_career=pdata.get("floor_career"),
|
||||||
|
floor_league=league_averages.get("consistency_floor", 0.0),
|
||||||
|
stability_current=pdata.get("stability_current"),
|
||||||
|
stability_career=pdata.get("stability_career"),
|
||||||
|
stability_league=league_averages.get("consistency_stability", 0.0),
|
||||||
|
average_current=pdata.get("average_current"),
|
||||||
|
average_career=pdata.get("average_career"),
|
||||||
|
average_league=league_averages.get("consistency_average", 0.0),
|
||||||
|
matches_played_current=pdata["career"].get("maps", 0),
|
||||||
|
percentile=None
|
||||||
|
)
|
||||||
|
pdata["consistency"] = {
|
||||||
|
"raw": consistency_result.normalized_score,
|
||||||
|
"summary": consistency_result.summary_short
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clutch
|
||||||
|
pdata["clutch"] = compute_clutch(
|
||||||
|
pdata,
|
||||||
|
league_averages,
|
||||||
|
clutch_distribution
|
||||||
|
)
|
||||||
|
|
||||||
|
# Support Specialist
|
||||||
|
pdata["support_specialist"] = build_support_specialist_tag(
|
||||||
|
pdata,
|
||||||
|
league_averages,
|
||||||
|
support_distribution
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Consistency (Hybrid Career Tag - New System)
|
# Consistency (Hybrid Career Tag - New System)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ def compute_league_metrics(all_players):
|
||||||
"push_time": [],
|
"push_time": [],
|
||||||
"headshot_rate": [],
|
"headshot_rate": [],
|
||||||
"pressure_eff": [],
|
"pressure_eff": [],
|
||||||
|
"deaths": [],
|
||||||
|
"dom_actions": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
|
|
@ -41,10 +43,11 @@ def compute_league_metrics(all_players):
|
||||||
"payload": [],
|
"payload": [],
|
||||||
"consistency": [],
|
"consistency": [],
|
||||||
"domination": [],
|
"domination": [],
|
||||||
|
"support_specialist": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
# Build league averages
|
# Build league averages (PER-MAP NORMALIZED)
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
for p in all_players:
|
for p in all_players:
|
||||||
c = p.get("career", {})
|
c = p.get("career", {})
|
||||||
|
|
@ -52,13 +55,17 @@ def compute_league_metrics(all_players):
|
||||||
if maps <= 0:
|
if maps <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
kills = c.get("kills", 0)
|
# Per-map normalization
|
||||||
deaths = c.get("deaths", 0)
|
kills = c.get("kills", 0) / maps
|
||||||
damage = c.get("damage", 0)
|
deaths = c.get("deaths", 0) / maps
|
||||||
|
damage = c.get("damage", 0) / maps
|
||||||
|
push = c.get("push_time", 0) / maps
|
||||||
|
captures = c.get("captures", c.get("DOM_Captures", 0)) / maps
|
||||||
|
counters = c.get("counters", c.get("DOM_Counters", 0)) / maps
|
||||||
|
|
||||||
shots = c.get("shots", 0)
|
shots = c.get("shots", 0)
|
||||||
shots_hit = c.get("shots_hit", 0)
|
shots_hit = c.get("shots_hit", 0)
|
||||||
headshots = c.get("headshots", 0)
|
headshots = c.get("headshots", 0)
|
||||||
push = c.get("push_time", 0)
|
|
||||||
|
|
||||||
KD = kills / deaths if deaths > 0 else kills
|
KD = kills / deaths if deaths > 0 else kills
|
||||||
accuracy = shots_hit / shots if shots > 0 else 0
|
accuracy = shots_hit / shots if shots > 0 else 0
|
||||||
|
|
@ -71,6 +78,8 @@ def compute_league_metrics(all_players):
|
||||||
acc["push_time"].append(push)
|
acc["push_time"].append(push)
|
||||||
acc["headshot_rate"].append(headshot_rate)
|
acc["headshot_rate"].append(headshot_rate)
|
||||||
acc["pressure_eff"].append(pressure_eff)
|
acc["pressure_eff"].append(pressure_eff)
|
||||||
|
acc["deaths"].append(deaths)
|
||||||
|
acc["dom_actions"].append(captures + counters)
|
||||||
|
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
# Compute league averages
|
# Compute league averages
|
||||||
|
|
@ -102,6 +111,12 @@ def compute_league_metrics(all_players):
|
||||||
|
|
||||||
# Domination
|
# Domination
|
||||||
dist["domination"].append(compute_dom_raw(p))
|
dist["domination"].append(compute_dom_raw(p))
|
||||||
|
|
||||||
|
from tags.support_specialist import compute_support_specialist
|
||||||
|
|
||||||
|
dist["support_specialist"].append(
|
||||||
|
compute_support_specialist(p, league_averages)
|
||||||
|
)
|
||||||
|
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
# Sort distributions
|
# Sort distributions
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
|
|
||||||
|
|
||||||
import bisect
|
import bisect
|
||||||
|
from tags.support_specialist import build_support_specialist_tag
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Percentile + Tier Helpers
|
# Percentile + Tier Helpers
|
||||||
|
|
|
||||||
|
|
@ -231,4 +231,39 @@ def compute_slayer_prediction(team_a_players, team_b_players):
|
||||||
"teamB_avg": teamB_avg,
|
"teamB_avg": teamB_avg,
|
||||||
"teamA_win": round(pA * 100),
|
"teamA_win": round(pA * 100),
|
||||||
"teamB_win": round(pB * 100),
|
"teamB_win": round(pB * 100),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rank_match_players_support_specialist(players):
|
||||||
|
"""
|
||||||
|
Ranks players by Support Specialist (career tag) using percentile.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ranked = []
|
||||||
|
|
||||||
|
for p in players:
|
||||||
|
tag = p.get("support_specialist", {})
|
||||||
|
|
||||||
|
# Use percentile as the ranking basis
|
||||||
|
pct = tag.get("pct")
|
||||||
|
if pct is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ranked.append({
|
||||||
|
"name": p.get("name", "Unknown"),
|
||||||
|
"team": p.get("team", ""),
|
||||||
|
"pct": pct,
|
||||||
|
"tier": tag.get("tier", "D"),
|
||||||
|
"summary": tag.get("summary", ""),
|
||||||
|
"score_raw": pct, # used for team totals
|
||||||
|
"score_display": f"{pct:.1f}", # what you see in the table
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort descending by percentile
|
||||||
|
ranked.sort(key=lambda x: x["pct"], reverse=True)
|
||||||
|
|
||||||
|
# Assign ranks
|
||||||
|
for i, entry in enumerate(ranked, start=1):
|
||||||
|
entry["rank"] = i
|
||||||
|
|
||||||
|
return ranked
|
||||||
36
rankings.py
36
rankings.py
|
|
@ -106,4 +106,40 @@ def rank_match_players_clutch(players):
|
||||||
for i, r in enumerate(ranked, start=1):
|
for i, r in enumerate(ranked, start=1):
|
||||||
r["rank"] = i
|
r["rank"] = i
|
||||||
|
|
||||||
|
return ranked
|
||||||
|
|
||||||
|
|
||||||
|
def rank_match_players_support_specialist(players):
|
||||||
|
"""
|
||||||
|
Ranks players by Support Specialist (career tag).
|
||||||
|
Uses the already-computed tag stored in each player's data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ranked = []
|
||||||
|
|
||||||
|
for p in players:
|
||||||
|
tag = p.get("support_specialist", {})
|
||||||
|
raw = tag.get("raw")
|
||||||
|
|
||||||
|
# Skip players with no data
|
||||||
|
if raw is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ranked.append({
|
||||||
|
"name": p.get("name", "Unknown"),
|
||||||
|
"team": p.get("team", ""),
|
||||||
|
"raw": raw,
|
||||||
|
"pct": tag.get("pct", 0.0),
|
||||||
|
"tier": tag.get("tier", "D"),
|
||||||
|
"summary": tag.get("summary", ""),
|
||||||
|
"score_display": f"{raw:.2f}",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort descending by raw score
|
||||||
|
ranked.sort(key=lambda x: x["raw"], reverse=True)
|
||||||
|
|
||||||
|
# Assign ranks
|
||||||
|
for i, entry in enumerate(ranked, start=1):
|
||||||
|
entry["rank"] = i
|
||||||
|
|
||||||
return ranked
|
return ranked
|
||||||
77
tags/support_specialist.py
Normal file
77
tags/support_specialist.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
from utils.safe_tag import safe_raw
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# RAW SCORE CALCULATION
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def compute_support_specialist(player, league_averages):
|
||||||
|
c = player.get("career", {})
|
||||||
|
|
||||||
|
dmg = safe_raw(c.get("damage"))
|
||||||
|
deaths = safe_raw(c.get("deaths"))
|
||||||
|
push_time = safe_raw(c.get("push_time"))
|
||||||
|
dom_caps = safe_raw(c.get("captures"))
|
||||||
|
dom_counters = safe_raw(c.get("counters"))
|
||||||
|
|
||||||
|
# League anchors
|
||||||
|
league_push = max(1.0, league_averages.get("push_time", 1.0))
|
||||||
|
league_dom_actions = max(1.0, league_averages.get("dom_actions", 1.0))
|
||||||
|
league_damage = max(1.0, league_averages.get("damage", 1.0))
|
||||||
|
league_deaths = max(1.0, league_averages.get("deaths", 1.0))
|
||||||
|
league_pressure = max(1e-6, league_averages.get("pressure_eff", 1.0))
|
||||||
|
|
||||||
|
# Objective presence (relative to league, capped)
|
||||||
|
presence_payload = (push_time / league_push)
|
||||||
|
presence_dom = ((dom_caps + dom_counters) / league_dom_actions)
|
||||||
|
objective_presence = max(presence_payload, presence_dom)
|
||||||
|
objective_presence = max(0.0, min(objective_presence, 2.0))
|
||||||
|
|
||||||
|
# Survivability: pressure vs league pressure_eff
|
||||||
|
player_pressure = dmg / max(1.0, deaths)
|
||||||
|
survivability = player_pressure / league_pressure
|
||||||
|
survivability = max(0.0, min(survivability, 2.0))
|
||||||
|
|
||||||
|
# Damage component: relative to league damage
|
||||||
|
damage_component = dmg / league_damage
|
||||||
|
damage_component = max(0.0, min(damage_component, 2.0))
|
||||||
|
|
||||||
|
# Low-death bonus: 1 is good, 0 is bad
|
||||||
|
low_death_bonus = 1.0 - (deaths / league_deaths)
|
||||||
|
low_death_bonus = max(0.0, min(low_death_bonus, 1.0))
|
||||||
|
|
||||||
|
# Weighted sum, then scale back into 0–1
|
||||||
|
raw = (
|
||||||
|
0.35 * objective_presence +
|
||||||
|
0.35 * survivability +
|
||||||
|
0.20 * damage_component +
|
||||||
|
0.10 * low_death_bonus
|
||||||
|
) / 2.0 # max of the capped components is 2 → divide by 2 to keep ≤ 1
|
||||||
|
|
||||||
|
return raw
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# TAG BUILDER (RAW → PERCENTILE → TIER → SUMMARY)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def build_support_specialist_tag(player, league_averages, distribution):
|
||||||
|
"""
|
||||||
|
Converts raw Support Specialist score into full tag output.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tags.tag_framework import build_tag_output
|
||||||
|
|
||||||
|
raw = compute_support_specialist(player, league_averages)
|
||||||
|
|
||||||
|
def summary_fn(tier, pct):
|
||||||
|
if pct >= 85:
|
||||||
|
return "Elite support presence — stabilizes fights and enables strong objective pushes."
|
||||||
|
if pct >= 70:
|
||||||
|
return "Reliable support player with strong survivability and objective presence."
|
||||||
|
if pct >= 50:
|
||||||
|
return "Provides steady support value through survivability and objective actions."
|
||||||
|
if pct >= 30:
|
||||||
|
return "Occasional support impact but inconsistent objective presence."
|
||||||
|
return "Limited support impact — low survivability and minimal objective presence."
|
||||||
|
|
||||||
|
return build_tag_output(raw, distribution, summary_fn)
|
||||||
82
utils/safe_tag.py
Normal file
82
utils/safe_tag.py
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
def safe_pct(value):
|
||||||
|
"""
|
||||||
|
Ensures pct is always a float between 0 and 100.
|
||||||
|
None, NaN, or invalid → 0.0
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
return 0.0
|
||||||
|
v = float(value)
|
||||||
|
if math.isnan(v):
|
||||||
|
return 0.0
|
||||||
|
return max(0.0, min(100.0, v))
|
||||||
|
except:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def safe_tier(value):
|
||||||
|
"""
|
||||||
|
Ensures tier is always a valid letter.
|
||||||
|
None or empty → 'D'
|
||||||
|
"""
|
||||||
|
if value in (None, "", "None"):
|
||||||
|
return "D"
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_summary(value):
|
||||||
|
"""
|
||||||
|
Ensures summary is always a string.
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_raw(value):
|
||||||
|
"""
|
||||||
|
Ensures raw is always a float.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
return 0.0
|
||||||
|
v = float(value)
|
||||||
|
if math.isnan(v):
|
||||||
|
return 0.0
|
||||||
|
return v
|
||||||
|
except:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def safe_tag(tag_dict):
|
||||||
|
"""
|
||||||
|
Normalizes an entire tag dict.
|
||||||
|
Guarantees all fields exist and are safe.
|
||||||
|
"""
|
||||||
|
if not isinstance(tag_dict, dict):
|
||||||
|
return {
|
||||||
|
"raw": 0.0,
|
||||||
|
"pct": 0.0,
|
||||||
|
"tier": "D",
|
||||||
|
"summary": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"raw": safe_raw(tag_dict.get("raw")),
|
||||||
|
"pct": safe_pct(tag_dict.get("pct")),
|
||||||
|
"tier": safe_tier(tag_dict.get("tier")),
|
||||||
|
"summary": safe_summary(tag_dict.get("summary"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def safe_avg(values):
|
||||||
|
"""
|
||||||
|
Averages a list of pct values safely.
|
||||||
|
None, NaN, invalid → treated as 0.0
|
||||||
|
"""
|
||||||
|
cleaned = [safe_pct(v) for v in values]
|
||||||
|
if not cleaned:
|
||||||
|
return 0.0
|
||||||
|
return sum(cleaned) / len(cleaned)
|
||||||
Loading…
Reference in a new issue