Closed DesiPilla closed 12 months ago
Based on research done back on Nov 22, 2022
Football
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.
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?
I believe that statId : 155
(Team win) is now statID: 156
.
Clarifying that statSourceId = 0
represents single-week stats and statSourceId = 1
represents season-long stats.
We can define active_status
as:
active
: the player had a game and participated in itinactive
: the player had a game but did not participate in it (due to injury or suspension)bye
: the player did not have a game that weekThis 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"
Attempting to solve this on the open source repo: https://github.com/cwendt94/espn-api/pull/502
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()
insrc/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.