Source code for awpy.analytics.stats

"""Functions to calculate statistics for a player or team from a demofile.

    Typical usage example:

    from awpy.parser import DemoParser
    from awpy.analytics.stats import player_stats

    # Create the parser object.
    parser = DemoParser(
        demofile = "astralis-vs-liquid-m2-nuke.dem",
        demo_id = "AST-TL-BLAST2019-nuke",
        parse_frames=False,
    )

    # Parse the demofile, output results to a dictionary of dataframes.
    data = parser.parse()
    player_stats_json = player_stats(data["gameRounds"])
    player_stats_json[76561197999004010]

    https://github.com/pnxenopoulos/awpy/blob/main/examples/01_Basic_CSGO_Analysis.ipynb
"""
from typing import Union, cast, Literal, Optional
import pandas as pd
from awpy.types import GameRound, PlayerStatistics

# accuracy
# kast
# adr
# kill stats
# flash stats
# econ stats
[docs]def other_side(side: Literal["CT", "T"]) -> Literal["T", "CT"]: """Takes a csgo side as input and returns the opposite side in the same formatting Args: side (string): A csgo team side (t or ct all upper or all lower case) Returns: A string of the opposite team side in the same formatting as the input Raises: ValueError: Raises a ValueError if side not neither 'CT' nor 'T'""" if side == "CT": return "T" elif side == "T": return "CT" raise ValueError("side has to be either 'CT' or 'T'")
[docs]def player_stats( game_rounds: list[GameRound], return_type: str = "json", selected_side: str = "all" ) -> Union[dict[str, PlayerStatistics], pd.DataFrame]: """Generates a stats summary for a list of game rounds as produced by the DemoParser Args: game_rounds (list[GameRound]): List of game rounds as produced by the DemoParser return_type (str, optional): Return format ("json" or "df"). Defaults to "json". selected_side (str, optional): Which side(s) to consider. Defaults to "all". Other options are "CT" and "T" Returns: Union[dict[str, PlayerStatistics],pd.Dataframe]: Dictionary or Dataframe containing player information """ player_statistics: dict[str, PlayerStatistics] = {} player_statistics = {} selected_side = selected_side.upper() if selected_side in {"CT", "T"}: selected_side = cast(Literal["CT", "T"], selected_side) active_sides: set[Literal["CT", "T"]] = {selected_side} else: active_sides = {"CT", "T"} for r in game_rounds: # Add players kast: dict[str, dict[str, bool]] = {} round_kills = {} for side in [team.lower() + "Side" for team in active_sides]: side = cast(Literal["ctSide", "tSide"], side) for p in r[side]["players"] or []: player_key = p["playerName"] if p["steamID"] == 0 else str(p["steamID"]) if player_key not in player_statistics: player_statistics[player_key] = { "steamID": p["steamID"], "playerName": p["playerName"], "teamName": r[side]["teamName"], "isBot": p["steamID"] == 0, "totalRounds": 0, "kills": 0, "deaths": 0, "kdr": 0, "assists": 0, "tradeKills": 0, "tradedDeaths": 0, "teamKills": 0, "suicides": 0, "flashAssists": 0, "totalDamageGiven": 0, "totalDamageTaken": 0, "totalTeamDamageGiven": 0, "adr": 0, "totalShots": 0, "shotsHit": 0, "accuracy": 0, "rating": 0, "kast": 0, "hs": 0, "hsPercent": 0, "firstKills": 0, "firstDeaths": 0, "utilityDamage": 0, "smokesThrown": 0, "flashesThrown": 0, "heThrown": 0, "fireThrown": 0, "enemiesFlashed": 0, "teammatesFlashed": 0, "blindTime": 0, "plants": 0, "defuses": 0, "kills0": 0, "kills1": 0, "kills2": 0, "kills3": 0, "kills4": 0, "kills5": 0, "attempts1v1": 0, "success1v1": 0, "attempts1v2": 0, "success1v2": 0, "attempts1v3": 0, "success1v3": 0, "attempts1v4": 0, "success1v4": 0, "attempts1v5": 0, "success1v5": 0, } player_statistics[player_key]["totalRounds"] += 1 kast[player_key] = {} kast[player_key]["k"] = False kast[player_key]["a"] = False kast[player_key]["s"] = True kast[player_key]["t"] = False round_kills[player_key] = 0 # Calculate kills players_killed: dict[Literal["CT", "T"], set[str]] = { "T": set(), "CT": set(), } is_clutching: set[Optional[str]] = set() for k in r["kills"] or []: killer_key = ( str(k["attackerName"]) if str(k["attackerSteamID"]) == 0 else str(k["attackerSteamID"]) ) victim_key = ( str(k["victimName"]) if str(k["victimSteamID"]) == 0 else str(k["victimSteamID"]) ) assister_key = ( str(k["assisterName"]) if str(k["assisterSteamID"]) == 0 else str(k["assisterSteamID"]) ) flashthrower_key = ( str(k["flashThrowerName"]) if str(k["flashThrowerSteamID"]) == 0 else str(k["flashThrowerSteamID"]) ) if k["victimSide"] in players_killed: players_killed[k["victimSide"]].add(victim_key) # type: ignore[index] # Purely attacker related stats if ( k["attackerSide"] in active_sides and killer_key in player_statistics and k["attackerSteamID"] ): if not k["isSuicide"] and not k["isTeamkill"]: player_statistics[killer_key]["kills"] += 1 round_kills[killer_key] += 1 kast[killer_key]["k"] = True if k["isTeamkill"]: player_statistics[killer_key]["teamKills"] += 1 if k["isHeadshot"]: player_statistics[killer_key]["hs"] += 1 # Purely victim related stats: if victim_key in player_statistics and k["victimSide"] in active_sides: player_statistics[victim_key]["deaths"] += 1 kast[victim_key]["s"] = False if k["isSuicide"]: player_statistics[victim_key]["suicides"] += 1 if ( k["victimSide"] in active_sides # mypy does not understand that after the first part k["victimSide"] is Literal["CT", "T"] # and that `k["victimSide"].lower() + "Side"` leads to a Literal["ctSide", "tSide"] and len(r[k["victimSide"].lower() + "Side"]["players"]) # type: ignore[literal-required] - len(players_killed[k["victimSide"]]) # type: ignore[literal-required, index] == 1 ): for player in r[k["victimSide"].lower() + "Side"]["players"]: # type: ignore[literal-required] clutcher_key = ( str(player["playermName"]) if str(player["steamID"]) == 0 else str(player["steamID"]) ) if ( clutcher_key not in players_killed[k["victimSide"]] # type: ignore[literal-required, index] and clutcher_key not in is_clutching and clutcher_key in player_statistics ): is_clutching.add(clutcher_key) enemies_alive = len( r[other_side(k["victimSide"]).lower() + "Side"]["players"] # type: ignore ) - len( players_killed[other_side(k["victimSide"])] # type: ignore ) if enemies_alive > 0: player_statistics[clutcher_key][ f"attempts1v{enemies_alive}" # type: ignore[literal-required] ] += 1 if r["winningSide"] == k["victimSide"]: player_statistics[clutcher_key][ f"success1v{enemies_alive}" # type: ignore[literal-required] ] += 1 if k["isTrade"]: # A trade is always onto an enemy # If your teammate kills someone and then you kill them # -> that is not a trade kill for you # If you kill someone and then yourself # -> that is not a trade kill for you if ( k["attackerSide"] != k["victimSide"] and k["attackerSide"] in active_sides and killer_key in player_statistics and k["attackerSteamID"] ): player_statistics[killer_key]["tradeKills"] += 1 # Enemies CAN trade your own death # If you force an enemy to teamkill their mate after your death # -> thats a traded death for you # If you force your killer to kill themselves (in their own molo/nade/fall) # -> that is a traded death for you traded_key = ( k["playerTradedName"] if str(k["playerTradedSteamID"]) == 0 else str(k["playerTradedSteamID"]) ) # In most cases the traded player is on the same team as the trader # However in the above scenarios the opposite can be the case # So it is not enough to know that the trading player and # their side is initialized if ( k["playerTradedSide"] in active_sides and traded_key in player_statistics and k["playerTradedSteamID"] ): kast[traded_key]["t"] = True player_statistics[traded_key]["tradedDeaths"] += 1 if ( k["assisterSteamID"] and k["assisterSide"] != k["victimSide"] and assister_key in player_statistics and k["assisterSide"] in active_sides ): player_statistics[assister_key]["assists"] += 1 kast[assister_key]["a"] = True if ( k["flashThrowerSteamID"] and k["flashThrowerSide"] != k["victimSide"] and flashthrower_key in player_statistics and k["flashThrowerSide"] in active_sides ): player_statistics[flashthrower_key]["flashAssists"] += 1 kast[flashthrower_key]["a"] = True if k["isFirstKill"] and k["attackerSteamID"]: if ( k["attackerSide"] in active_sides and killer_key in player_statistics ): player_statistics[killer_key]["firstKills"] += 1 if k["victimSide"] in active_sides and victim_key in player_statistics: player_statistics[victim_key]["firstDeaths"] += 1 for d in r["damages"] or []: attacker_key = ( str(d["attackerName"]) if str(d["attackerSteamID"]) == 0 else str(d["attackerSteamID"]) ) victim_key = ( str(d["victimName"]) if str(d["victimSteamID"]) == 0 else str(d["victimSteamID"]) ) # Purely attacker related stats if ( d["attackerSide"] in active_sides and attacker_key in player_statistics and d["attackerSteamID"] ): if not d["isFriendlyFire"]: player_statistics[attacker_key]["totalDamageGiven"] += d[ "hpDamageTaken" ] else: # d["isFriendlyFire"]: player_statistics[attacker_key]["totalTeamDamageGiven"] += d[ "hpDamageTaken" ] if d["weaponClass"] not in ["Unknown", "Grenade", "Equipment"]: player_statistics[attacker_key]["shotsHit"] += 1 if d["weaponClass"] == "Grenade": player_statistics[attacker_key]["utilityDamage"] += d[ "hpDamageTaken" ] if ( d["victimSteamID"] and victim_key in player_statistics and d["victimSide"] in active_sides ): player_statistics[victim_key]["totalDamageTaken"] += d["hpDamageTaken"] for w in r["weaponFires"] or []: fire_key = ( w["playerName"] if w["playerSteamID"] == 0 else str(w["playerSteamID"]) ) if fire_key in player_statistics and w["playerSide"] in active_sides: player_statistics[fire_key]["totalShots"] += 1 for f in r["flashes"] or []: flasher_key = ( str(f["attackerName"]) if str(f["attackerSteamID"]) == 0 else str(f["attackerSteamID"]) ) player_key = ( str(f["playerName"]) if str(f["playerSteamID"]) == 0 else str(f["playerSteamID"]) ) if ( f["attackerSteamID"] and flasher_key in player_statistics and f["attackerSide"] in active_sides ): if f["attackerSide"] == f["playerSide"]: player_statistics[flasher_key]["teammatesFlashed"] += 1 else: player_statistics[flasher_key]["enemiesFlashed"] += 1 player_statistics[flasher_key]["blindTime"] += ( 0 if f["flashDuration"] is None else f["flashDuration"] ) for g in r["grenades"] or []: thrower_key = ( g["throwerName"] if g["throwerSteamID"] == 0 else str(g["throwerSteamID"]) ) if ( g["throwerSteamID"] and thrower_key in player_statistics and g["throwerSide"] in active_sides ): if g["grenadeType"] == "Smoke Grenade": player_statistics[thrower_key]["smokesThrown"] += 1 if g["grenadeType"] == "Flashbang": player_statistics[thrower_key]["flashesThrown"] += 1 if g["grenadeType"] == "HE Grenade": player_statistics[thrower_key]["heThrown"] += 1 if g["grenadeType"] in ["Incendiary Grenade", "Molotov"]: player_statistics[thrower_key]["fireThrown"] += 1 for b in r["bombEvents"] or []: player_key = ( b["playerName"] if b["playerSteamID"] == 0 else str(b["playerSteamID"]) ) if b["playerSteamID"] and player_key in player_statistics: if b["bombAction"] == "plant" and "T" in active_sides: player_statistics[player_key]["plants"] += 1 if b["bombAction"] == "defuse" and "CT" in active_sides: player_statistics[player_key]["defuses"] += 1 for player, components in kast.items(): if any(components.values()): player_statistics[player]["kast"] += 1 for player, n_kills in round_kills.items(): if n_kills in range(6): # 0, 1, 2, 3, 4, 5 player_statistics[player][f"kills{n_kills}"] += 1 # type: ignore[literal-required] for player in player_statistics.values(): player["kast"] = round( 100 * player["kast"] / player["totalRounds"], 1, ) player["blindTime"] = round(player["blindTime"], 2) player["kdr"] = round( player["kills"] / player["deaths"] if player["deaths"] != 0 else player["kills"], 2, ) player["adr"] = round( player["totalDamageGiven"] / player["totalRounds"], 1, ) player["accuracy"] = round( player["shotsHit"] / player["totalShots"] if player["totalShots"] != 0 else 0, 2, ) player["hsPercent"] = round( player["hs"] / player["kills"] if player["kills"] != 0 else 0, 2, ) impact = ( 2.13 * (player["kills"] / player["totalRounds"]) + 0.42 * (player["assists"] / player["totalRounds"]) - 0.41 ) player["rating"] = ( 0.0073 * player["kast"] + 0.3591 * (player["kills"] / player["totalRounds"]) - 0.5329 * (player["deaths"] / player["totalRounds"]) + 0.2372 * (impact) + 0.0032 * (player["adr"]) + 0.1587 ) player["rating"] = round(player["rating"], 2) if return_type == "df": return ( pd.DataFrame() .from_dict(player_statistics, orient="index") .reset_index(drop=True) ) else: return player_statistics