DesiPilla / espn-api-v3

This project aims to make ESPN Fantasy Football statistics easily available. With the introduction of version 3 of the ESPN's API, this structure creates leagues, teams, and player classes that allow for advanced data analytics and the potential for many new features to be added.
MIT License
28 stars 10 forks source link

Get number of players who were injured during a week #15

Closed DesiPilla closed 12 months ago

DesiPilla commented 2 years ago

It would be helpful to know, from a given lineup, which players did not play due to injury. There is a skeleton function get_num_out() in src/doritostats/analytic_utils but there is no actual code to do so.

https://github.com/DesiPilla/espn-api-v3/blob/2b43d1c437ed8968123423002f2fddc61c54bf90/src/doritostats/analytic_utils.py#L88-L92

This information could be another award given out in fantasy_stats/views.py - django_weekly_stats(). This information could also contribute to a team's luck index.

DesiPilla commented 1 year ago

Based on research done back on Nov 22, 2022

Sport

Football

Summary

Okay so for a while now, an issue we've had is that ESPN only return's a player's current health status, even when querying a previous week or year. I think I have a way to get around this and assume a player's health status from previous weeks.

Example REST call: https://fantasy.espn.com/apis/v3/games/ffl/seasons/2022/segments/0/leagues/1086064?view=mMatchupScore&view=mScoreboard&scoringPeriodId=6 (I know this isn't that far back, but it still works).

LAC WR Keenan Allen was OUT due to injury. His player json looks like:

{
    "injuryStatus": "NORMAL",
    "lineupSlotId": 20,
    "playerId": 15818,
    "playerPoolEntry": {
    "appliedStatTotal": 0.0,
    "id": 15818,
    "onTeamId": 1,
    "player": {
        "active": true,
        "defaultPositionId": 3,
        "eligibleSlots": [3, 4, 5, 23, 7, 20, 21],
        "firstName": "Keenan",
        "fullName": "Keenan Allen",
        "id": 15818,
        "injured": false,
        "injuryStatus": "QUESTIONABLE",
        "jersey": "13",
        "lastName": "Allen",
        "proTeamId": 24,
        "stats": [
        {
            "appliedStats": {},
            "appliedTotal": 0.0,
            "appliedTotalCeiling": 0.0,
            "externalId": "20226",
            "id": "1120226",
            "lastUpdateInfo": {},
            "proTeamId": 0,
            "scoringPeriodId": 6,
            "seasonId": 2022,
            "statSourceId": 1,
            "statSplitTypeId": 1,
            "stats": {},
            "variance": {
            "23": 0.572518801,
            "24": 5.888548208,
            "25": 0.016224972,
            "26": 0.187113084,
            "35": 0.07,
            "36": 0.06,
            "42": 36.81194703,
            "43": 0.565194165,
            "44": 0.279276063,
            "45": 0.156779565,
            "46": 0.18,
            "53": 3.011551833,
            "58": 3.509159092,
            "63": 0.001,
            "68": 0.339298477,
            "72": 0.190029238
            }
        },
        {
            "appliedStats": {},
            "appliedTotal": 0.0,
            "externalId": "401437790",
            "id": "01401437790",
            "lastUpdateInfo": {},
            "proTeamId": 24,
            "scoringPeriodId": 6,
            "seasonId": 2022,
            "statSourceId": 0,
            "statSplitTypeId": 1,
            "stats": {},
            "variance": {}
        }
        ],
        "universeId": 1
    },
    "status": "ONTEAM"
    },
    "status": "NORMAL"
},

As we see, "injured": false and "injuryStatus": "QUESTIONABLE" are not reflective of his actual out status in Week 6. However, resp["player"]["stats"][1]["stats"] = {} is an empty dictionary. I believe that this indicates that the player was OUT that week. Note: We must look at the stat dictionary with "statSourceId": 1. I think this refers to single-week stats, vs season-long stats.

For example, here's the json for NYJ WR Elijah Moore from that week.

{
    "injuryStatus": "NORMAL",
    "lineupSlotId": 20,
    "playerId": 4372414,
    "playerPoolEntry": {
    "appliedStatTotal": 0.0,
    "id": 4372414,
    "onTeamId": 4,
    "player": {
        "active": true,
        "defaultPositionId": 3,
        "eligibleSlots": [3, 4, 5, 23, 7, 20, 21],
        "firstName": "Elijah",
        "fullName": "Elijah Moore",
        "id": 4372414,
        "injured": false,
        "injuryStatus": "ACTIVE",
        "jersey": "8",
        "lastName": "Moore",
        "proTeamId": 20,
        "stats": [
        {
            "appliedStats": {},
            "appliedTotal": 0.0,
            "externalId": "401437780",
            "id": "01401437780",
            "lastUpdateInfo": {},
            "proTeamId": 20,
            "scoringPeriodId": 6,
            "seasonId": 2022,
            "statSourceId": 0,
            "statSplitTypeId": 1,
            "stats": { "155": 1.0, "210": 1.0 },
            "variance": {}
        },
        {
            "appliedStats": {
            "53": 1.8088437515,
            "72": -0.068263156,
            "24": 0.3093177588,
            "25": 0.097625838,
            "26": 0.001402,
            "42": 5.059137268000001,
            "43": 1.4621709059999999,
            "44": 0.021829334,
            "63": 0.001644
            },
            "appliedTotal": 8.6937077003,
            "appliedTotalCeiling": 19.296405354,
            "externalId": "20226",
            "id": "1120226",
            "lastUpdateInfo": {},
            "proTeamId": 0,
            "scoringPeriodId": 6,
            "seasonId": 2022,
            "statSourceId": 1,
            "statSplitTypeId": 1,
            "stats": {
            "23": 0.500451604,
            "24": 3.093177588,
            "25": 0.016270973,
            "26": 7.01e-4,
            "35": 9.22e-4,
            "36": 6.45e-4,
            "37": 3.97e-4,
            "38": 1.35e-5,
            "39": 6.180772654,
            "40": 3.093177588,
            "42": 50.59137268,
            "43": 0.243695151,
            "44": 0.010914667,
            "45": 0.011145444,
            "46": 0.007283547,
            "47": 10.0,
            "48": 5.0,
            "49": 2.0,
            "50": 2.0,
            "51": 1.0,
            "53": 3.617687503,
            "56": 0.099440855,
            "57": 0.00308,
            "58": 6.293878265,
            "60": 13.98445074,
            "61": 50.59137268,
            "62": 0.011615355,
            "63": 2.74e-4,
            "66": 0.00757,
            "67": 0.054730147,
            "68": 0.062301224,
            "70": 0.00348,
            "71": 0.030648882,
            "72": 0.034131578,
            "73": 0.034131578,
            "210": 1.0
            },
            "variance": {
            "23": 0.512347538,
            "24": 6.407027392,
            "25": 0.25,
            "26": 0.187113084,
            "35": 0.07,
            "36": 0.06,
            "42": 35.93831521,
            "43": 0.602079729,
            "44": 0.279276063,
            "45": 0.22,
            "46": 0.18,
            "53": 2.301267767,
            "58": 2.633122354,
            "63": 0.001,
            "68": 0.037512689,
            "72": 0.416863579
            }
        }
        ],
        "universeId": 1
    },
    "status": "ONTEAM"
    },
    "status": "NORMAL"
}

Moore played, but did not record a single stat. Yet, despite not registering any stats, his json shows resp["player"]["stats"][0]["stats"] = "stats": { "155": 1.0, "210": 1.0 }. For reference, statId 155 refers to "Team win" and 210 refers to "Games played". Because Moore was active for the game, these two stats appear in his json.

So, this would indicate that if a player's resp["player"]["stats"][0]["stats"] is an empty dictionary, then we can safely assume that the player was OUT that week. However, there are two other things to consider:

Here's the json for TEN RB Derrick Henry

{
    "injuryStatus": "NORMAL",
    "lineupSlotId": 20,
    "playerId": 3043078,
    "playerPoolEntry": {
    "appliedStatTotal": 0.0,
    "id": 3043078,
    "onTeamId": 8,
    "player": {
        "active": true,
        "defaultPositionId": 2,
        "eligibleSlots": [2, 3, 23, 7, 20, 21],
        "firstName": "Derrick",
        "fullName": "Derrick Henry",
        "id": 3043078,
        "injured": false,
        "injuryStatus": "ACTIVE",
        "jersey": "22",
        "lastName": "Henry",
        "proTeamId": 10,
        "stats": [
        {
            "appliedStats": {},
            "appliedTotal": 0.0,
            "appliedTotalCeiling": 0.0,
            "externalId": "20226",
            "id": "1120226",
            "lastUpdateInfo": {},
            "proTeamId": 0,
            "scoringPeriodId": 6,
            "seasonId": 2022,
            "statSourceId": 1,
            "statSplitTypeId": 1,
            "stats": {},
            "variance": {
            "23": 7.989211797,
            "24": 58.70602937,
            "25": 0.960427714,
            "26": 0.187113084,
            "35": 0.349889526,
            "36": 0.349889526,
            "42": 16.03528837,
            "43": 0.18778446,
            "44": 0.279276063,
            "45": 0.167801519,
            "46": 0.167801519,
            "53": 1.198000097,
            "58": 1.384393089,
            "63": 0.001,
            "68": 0.397612623,
            "72": 0.260540364
            }
        }
        ],
        "universeId": 1
    },
    "status": "ONTEAM"
    },
    "status": "NORMAL"
},

The Titans were on a bye during Week 6, so Henry did not play. His json also has "injuryStatus": "ACTIVE". However, recall that resp["player"]["stats"] is a list with 2 elements in it: "statSourceId": 0 is a stats dictionary containing single-week stats for the current week, and "statSourceId": 1 is a stats dictionary containing season-long stats. In Henry's json, len(resp["player"]["stats"]) = 1. He only has one stats dictionary, the one with "statSourceId": 1. This is because he did not play this week, and was not supposed to play. This is also the case for other players on bye, which to me makes it a reliable indicator that the player was on a bye and not injured.

Here's the json for ARI WR DeAndre Hopkins from Week 6

{
    "injuryStatus": "NORMAL",
    "lineupSlotId": 20,
    "playerId": 15795,
    "playerPoolEntry": {
    "appliedStatTotal": 0.0,
    "id": 15795,
    "onTeamId": 10,
    "player": {
        "active": true,
        "defaultPositionId": 3,
        "eligibleSlots": [3, 4, 5, 23, 7, 20, 21],
        "firstName": "DeAndre",
        "fullName": "DeAndre Hopkins",
        "id": 15795,
        "injured": false,
        "injuryStatus": "ACTIVE",
        "jersey": "10",
        "lastName": "Hopkins",
        "proTeamId": 22,
        "stats": [
        {
            "appliedStats": {},
            "appliedTotal": 0.0,
            "externalId": "401437787",
            "id": "01401437787",
            "lastUpdateInfo": {},
            "proTeamId": 22,
            "scoringPeriodId": 6,
            "seasonId": 2022,
            "statSourceId": 0,
            "statSplitTypeId": 1,
            "stats": {},
            "variance": {}
        },
        {
            "appliedStats": {},
            "appliedTotal": 0.0,
            "appliedTotalCeiling": 0.0,
            "externalId": "20226",
            "id": "1120226",
            "lastUpdateInfo": {},
            "proTeamId": 0,
            "scoringPeriodId": 6,
            "seasonId": 2022,
            "statSourceId": 1,
            "statSplitTypeId": 1,
            "stats": {},
            "variance": {
            "23": 0.226210458,
            "24": 1.755608773,
            "25": 0.016224972,
            "26": 0.187113084,
            "35": 0.07,
            "36": 0.06,
            "42": 41.18427934,
            "43": 0.716598572,
            "44": 0.279276063,
            "45": 0.244508154,
            "46": 0.16604118,
            "53": 2.526829014,
            "58": 3.328825781,
            "63": 0.001,
            "68": 0.310767721,
            "72": 0.27312011
            }
        }
        ],
        "universeId": 2
    },
    "status": "ONTEAM"
    },
    "status": "NORMAL"
}

First, let's note that Hopkins' json shows"injured": false and "injuryStatus": "ACTIVE" when it should show "injured":false and "injuryStatus":"SUSPENSION". This is because, today in Week 10, he is no longer suspended. In Week 6, though, Hopkins was still serving his 6-game suspension. BUT, we can see that resp["player"]["stats"][0]["stats"] = {}, which we previously decided safely meant that a player was OUT. This is the edge case. If the player is still suspended today, it becomes an easy fix (just check for "injuryStatus":"SUSPENSION"). But in a case like this, I don't know how to pull this data simply from the API.

Perhaps, a player being suspended is comparable to being OUT. It's not due to injury, but it's distinct from a bye. Players on suspension function more like those that are injured, since teams will stash them waiting until they come back from suspension. And when they do, you don't know how long it will take before they make meaningful contributions to the team (like injured players). Players returning from a bye don't really exhibit this behavior.

In conclusion

When looking at a player's json, if resp["player"]["stats"][0]["stats"] = {} (make sure it's the one with "statSourceId": 1, AND len(resp["player"]["stats"]) == 2, the player WAS OUT (or suspended).

Does this sound right?

DesiPilla commented 1 year ago

I believe that statId : 155 (Team win) is now statID: 156.

DesiPilla commented 1 year ago

Clarifying that statSourceId = 0 represents single-week stats and statSourceId = 1 represents season-long stats.

DesiPilla commented 1 year ago

We can define active_status as:

This logic passes the following tests for players in 2023:

def get_player_json(player_id: int, week: int) -> dict:
    endpoint = "{}view=mMatchupScore&view=mScoreboard&scoringPeriodId={}".format(
        league.endpoint, week
    )
    resp = requests.get(endpoint, cookies=league.cookies).json()

    matchups = [
        matchup for matchup in resp["schedule"] if matchup["matchupPeriodId"] == week
    ]
    for matchup in matchups:
        for team in [matchup["away"], matchup["home"]]:
            for player in team["rosterForCurrentScoringPeriod"]["entries"]:
                if player["playerId"] == player_id:
                    return player["playerPoolEntry"]["player"]

def get_player_active_status(player_json: dict) -> str:
    """This function parses a player's JSON object for a specific week
    to reverse-engineer whether the player was active that week.

    At this time, it is not possible to distinguish between players ruled "OUT" due
    to injury vs suspension. Both are considered "inactive" in the ESPN API and
    have the same JSON format.

    Args:
        player_json (dict): A player's JSON object for a spceific week from the ESPN API

    Returns:
        str: A string indicating whether the player's status for the week ("active", "inactive", "bye")
    """
    # statsSourceId = 0 refers to the single-week stats
    # statsSourceId = 1 refers to the season-long stats
    # If there is no single-week stats dictinoary, the player was on a bye
    if not [stats for stats in player_json["stats"] if stats["statSourceId"] == 0]:
        return "bye"

    else:
        stats = [stats for stats in player_json["stats"] if stats["statSourceId"] == 0][
            0
        ]
        if not stats["stats"]:
            # If there is a single-week stats dictionary but it is empty, the player was injured or suspended (did not play in the game)
            return "inactive"
        else:
            # If there is a single-week stats dictionary and it is not empty, the player was active
            return "active"

player_id, week = 4430807, 2  # Active player (Bijan Robinson)
player = get_player_json(player_id, week)
assert get_player_active_status(player) == "active"

player_id, week = 4430807, 11  # Bye player (Bijan Robinson)
player = get_player_json(player_id, week)
assert get_player_active_status(player) == "bye"

player_id, week = 4242335, 2  # Injured player (Jonathan Taylor)
player = get_player_json(player_id, week)
assert get_player_active_status(player) == "inactive"

player_id, week = 4242335, 9  # No longer injured player (Jonathan Taylor)
player = get_player_json(player_id, week)
assert get_player_active_status(player) == "active"

player_id, week = 4429795, 2  # Active player (same team as suspended) (Jahmyr Gibbs)
player = get_player_json(player_id, week)
assert get_player_active_status(player) == "active"

player_id, week = 4426388, 2  # Suspended player (Jameson Williams)
player = get_player_json(player_id, week)
assert get_player_active_status(player) == "inactive"

player_id, week = 4426388, 7  # No longer suspended player (Jameson Williams)
player = get_player_json(player_id, week)
assert get_player_active_status(player) == "active"

player_id, week = 4239993, 1  # Active player who scored 0 points (Tee Higgins)
player = get_player_json(player_id, week)
assert get_player_active_status(player) == "active"
DesiPilla commented 1 year ago

Attempting to solve this on the open source repo: https://github.com/cwendt94/espn-api/pull/502

DesiPilla commented 12 months ago

With the release of espn-api 0.34.0, the PR became publicly available. A PR was merged adding this to the website.