If you have any questions about the code here, feel free to reach out to me on Twitter or on Reddit.

Advanced WR stats for top PPR receivers

For this notebook, let's put together a table that gives us some key fantasy football stats for WRs. Our table is going to include fantasy points per game, average targets, air yards, average target share in games played, average air yards in games played, weighted opportunity rating, and redzone looks. This is the data that I believe has the most fantasy football applicability. Everything else is probably secondary.

To top it off, we're also going to be including player headshots and team logos and add some dynamic highlighting to our DataFrame, similar to something you'd do in Excel.

Our data source is going to be nflfastR which contains NFL play by play data updated daily, to which I've set a Python package for easy loading in pandas. The cell below will install the external package in your notebook runtime.

The %%capture magic command here just supresses the output for our cell.

And as always, let's import some libraries we'll be needing for this project.

Loading NFL play by play data with the nflfastpy Python package is as easy as the one-liner below. The positional argument here is the season.

We can see we have 24,214 plays to work with at the time I'm writing this. If you're running this code in a week other than week 10 of the current NFL season, your results for the following data will vary.

What we want to do now is run a groupby and find each receivers yards gained, receiving touchdowns, receptions, air yards, and also their fantasy points scored. We can quickly add a column to our Dataframe using the assign method. Here, we're establishing a column of raw_rec_fpts. Raw receiving fantasy points is a player's points scored only because of receiving, which for receivers is going to be the bulk of their production. We don't account for fumbles, WR end arounds, WR trick passing plays, etc so the results may differ from your league formats fantasy points scored per game.

receiver_player_id posteam game_id pass_attempt yards_gained air_yards complete_pass pass_touchdown raw_rec_fpts
2168 32013030-2d30-3033-3633-373065ee5506 LAC 2020_08_LAC_DEN 1.0 -7.0 -5.0 1.0 0.0 0.3
1713 32013030-2d30-3033-3532-3433c2333695 PHI 2020_06_BAL_PHI 2.0 -6.0 17.0 1.0 0.0 0.4
690 32013030-2d30-3033-3233-393431c33be1 DEN 2020_01_TEN_DEN 2.0 -6.0 -5.0 1.0 0.0 0.4
564 32013030-2d30-3033-3138-30361fd87e06 LA 2020_05_LA_WAS 2.0 -6.0 -2.0 1.0 0.0 0.4
15 32013030-2d30-3032-3334-35395dc60da5 GB 2020_02_DET_GB 1.0 -6.0 -4.0 1.0 0.0 0.4

We want to now find each teams air yards thrown and targets thrown for each individual game, and then merge that data with the table above. We are then going to calculate an average target share and average air yards share for each receiver.

game_id posteam air_yards pass_attempt
0 2020_01_ARI_SF ARI 192.0 37.0
1 2020_01_ARI_SF SF 225.0 32.0
2 2020_01_CHI_DET CHI 353.0 36.0
3 2020_01_CHI_DET DET 372.0 41.0
4 2020_01_CLE_BAL BAL 255.0 25.0

And now wemerge the data on both the game_id and posteam column (posteam means team in possession of the football).

receiver_player_id posteam game_id pass_attempt_player yards_gained air_yards_player complete_pass pass_touchdown raw_rec_fpts air_yards_team pass_attempt_team
0 32013030-2d30-3032-3231-32373ce51f62 LV 2020_01_LV_CAR 1.0 2.0 2.0 1.0 0.0 1.2 161.0 28.0
1 32013030-2d30-3033-3135-3439d46a53e9 LV 2020_01_LV_CAR 1.0 23.0 23.0 1.0 1.0 9.3 161.0 28.0
2 32013030-2d30-3033-3136-3130db0aa3c4 LV 2020_01_LV_CAR 8.0 45.0 30.0 6.0 0.0 10.5 161.0 28.0
3 32013030-2d30-3033-3239-3732ec926fb5 LV 2020_01_LV_CAR 3.0 23.0 1.0 3.0 0.0 5.3 161.0 28.0
4 32013030-2d30-3033-3330-3235189aefb0 LV 2020_01_LV_CAR 1.0 15.0 6.0 1.0 0.0 2.5 161.0 28.0

Now that we have the data merged, we can calculate target share and air yards share by creating two new columns in our rec_df DataFrame.

receiver_player_id posteam game_id pass_attempt_player yards_gained air_yards_player complete_pass pass_touchdown raw_rec_fpts air_yards_team pass_attempt_team target_share air_yards_share
0 32013030-2d30-3032-3231-32373ce51f62 LV 2020_01_LV_CAR 1.0 2.0 2.0 1.0 0.0 1.2 161.0 28.0 0.035714 0.012422
1 32013030-2d30-3033-3135-3439d46a53e9 LV 2020_01_LV_CAR 1.0 23.0 23.0 1.0 1.0 9.3 161.0 28.0 0.035714 0.142857
2 32013030-2d30-3033-3136-3130db0aa3c4 LV 2020_01_LV_CAR 8.0 45.0 30.0 6.0 0.0 10.5 161.0 28.0 0.285714 0.186335
3 32013030-2d30-3033-3239-3732ec926fb5 LV 2020_01_LV_CAR 3.0 23.0 1.0 3.0 0.0 5.3 161.0 28.0 0.107143 0.006211
4 32013030-2d30-3033-3330-3235189aefb0 LV 2020_01_LV_CAR 1.0 15.0 6.0 1.0 0.0 2.5 161.0 28.0 0.035714 0.037267

Now we just want to grab the average results, which will give us back average raw receiving fantasy points scored, average target share, etc. etc.

receiver_player_id pass_attempt_player yards_gained air_yards_player complete_pass pass_touchdown raw_rec_fpts air_yards_team pass_attempt_team target_share air_yards_share
257 32013030-2d30-3033-3432-3836149d738d 13.00 184.000 120.000000 9.000000 1.000000 33.400000 224.000000 34.000 0.382353 0.535714
86 32013030-2d30-3033-3133-383188f79eec 11.50 112.500 111.166667 8.833333 1.333333 28.083333 287.666667 34.000 0.332879 0.400927
371 32013030-2d30-3033-3536-343097915ff1 8.50 98.500 123.625000 5.375000 1.000000 21.225000 313.875000 35.875 0.244524 0.414175
122 32013030-2d30-3033-3232-31312f766863 8.75 76.875 92.000000 6.625000 0.875000 19.562500 313.875000 35.875 0.236870 0.287703
365 32013030-2d30-3033-3535-3932f90568c6 8.80 87.000 99.000000 5.800000 0.800000 19.300000 330.400000 34.800 0.245506 0.302467

Let's quickly filter and rename some columns to clean up our data before proceeding to the next step.

receiver_player_id Avg. Raw Fpts Avg. Targets Avg. Air Yards Avg. Target Share Avg. Air Yards Share
257 32013030-2d30-3033-3432-3836149d738d 33.400000 13.00 120.000000 0.382353 0.535714
86 32013030-2d30-3033-3133-383188f79eec 28.083333 11.50 111.166667 0.332879 0.400927
371 32013030-2d30-3033-3536-343097915ff1 21.225000 8.50 123.625000 0.244524 0.414175
122 32013030-2d30-3033-3232-31312f766863 19.562500 8.75 92.000000 0.236870 0.287703
365 32013030-2d30-3033-3535-3932f90568c6 19.300000 8.80 99.000000 0.245506 0.302467

Below, we are going to grab redzone looks. A target is a redzone look if the difference between air yards and yardline_100 is 0. yardline_100 goes from 99 to 1, where 1 represents the goal line. The difference between air yards and this yard line column won't fall below 0, even if the ball is thrown, for example, in the back of the endzone (which would add 10 more yards in terms of air yards from the goal line). That was initial assumption, to which we'd use <= 0, but this is not the case. So here, we check if the difference is 0, and then sum the results.

receiver_player_id Redzone Looks
371 32013030-2d30-3033-3536-343097915ff1 10
315 32013030-2d30-3033-3438-333761eb5105 10
46 32013030-2d30-3033-3030-3335960ad201 10
122 32013030-2d30-3033-3232-31312f766863 9
340 32013030-2d30-3033-3532-32393f07fdc6 8

Now, let's merge this redzone data in to our original table.

receiver_player_id Avg. Raw Fpts Avg. Targets Avg. Air Yards Avg. Target Share Avg. Air Yards Share Redzone Looks
0 32013030-2d30-3033-3432-3836149d738d 33.400000 13.00 120.000000 0.382353 0.535714 0
1 32013030-2d30-3033-3133-383188f79eec 28.083333 11.50 111.166667 0.332879 0.400927 7
2 32013030-2d30-3033-3536-343097915ff1 21.225000 8.50 123.625000 0.244524 0.414175 10
3 32013030-2d30-3033-3232-31312f766863 19.562500 8.75 92.000000 0.236870 0.287703 9
4 32013030-2d30-3033-3535-3932f90568c6 19.300000 8.80 99.000000 0.245506 0.302467 7

You can see we're missing team names and receiver player names as a result of running aggregation functions like sum and mean that don't work with string columns. Let's grab a player id table where each player's id corresponds to their team name and actual name, and then merge the data back in to our original DataFrame.

receiver_player_id posteam receiver_player_name
0 32013030-2d30-3032-3231-32373ce51f62 LV J.Witten
1 32013030-2d30-3032-3239-323176c2a1fa ARI L.Fitzgerald
2 32013030-2d30-3032-3334-35395dc60da5 GB A.Rodgers
3 32013030-2d30-3032-3335-3030dd77cad3 NYJ F.Gore
4 32013030-2d30-3032-3336-3832b8755c76 MIA R.Fitzpatrick
receiver_player_name posteam receiver_player_id Avg. Raw Fpts Avg. Targets Avg. Air Yards Avg. Target Share Avg. Air Yards Share Redzone Looks
0 R.James SF 32013030-2d30-3033-3432-3836149d738d 33.400000 13.00 120.000000 0.382353 0.535714 0
1 D.Adams GB 32013030-2d30-3033-3133-383188f79eec 28.083333 11.50 111.166667 0.332879 0.400927 7
2 D.Metcalf SEA 32013030-2d30-3033-3536-343097915ff1 21.225000 8.50 123.625000 0.244524 0.414175 10
3 T.Lockett SEA 32013030-2d30-3033-3232-31312f766863 19.562500 8.75 92.000000 0.236870 0.287703 9
4 T.Fulgham PHI 32013030-2d30-3033-3535-3932f90568c6 19.300000 8.80 99.000000 0.245506 0.302467 7

Let's quickly rename some columns after proceeding once again.

Receiver Team receiver_player_id Avg. Raw Fpts Avg. Targets Avg. Air Yards Avg. Target Share Avg. Air Yards Share Redzone Looks
0 R.James SF 32013030-2d30-3033-3432-3836149d738d 33.400000 13.00 120.000000 0.382353 0.535714 0
1 D.Adams GB 32013030-2d30-3033-3133-383188f79eec 28.083333 11.50 111.166667 0.332879 0.400927 7
2 D.Metcalf SEA 32013030-2d30-3033-3536-343097915ff1 21.225000 8.50 123.625000 0.244524 0.414175 10
3 T.Lockett SEA 32013030-2d30-3033-3232-31312f766863 19.562500 8.75 92.000000 0.236870 0.287703 9
4 T.Fulgham PHI 32013030-2d30-3033-3535-3932f90568c6 19.300000 8.80 99.000000 0.245506 0.302467 7

In the cell below, we're doing two things. First we're calculating average WOPR, or weighted opportunity rating for each player, which is a weighted average of target share and air yards share. I talk about WOPR in previous posts.

Secondly, you can see in the table above that Richie James is the top receiver in terms of fantasy points per game. This may be true, but we don't want Richie James at the top of our table considering he's only had one big game and SF may be getting back some receivers in week 10, further limiting his value. In short, we want to make sure James is not at the top of the table by adding a total_fpts scored column to our DataFrame and then sorting the table in descending order via that column. For now, we are going to sort the table, but the table will come out of order again once we actually merge some additional tables. We'll sort again later.

Receiver Team receiver_player_id Avg. Raw Fpts Avg. Targets Avg. Air Yards Avg. Target Share Avg. Air Yards Share Redzone Looks Avg. WOPR total_fpts
5 T.Kelce KC 32013030-2d30-3033-3035-3036654ef292 18.988889 8.888889 67.000000 0.233923 0.230771 7 0.512424 170.9
2 D.Metcalf SEA 32013030-2d30-3033-3536-343097915ff1 21.225000 8.500000 123.625000 0.244524 0.414175 10 0.656708 169.8
1 D.Adams GB 32013030-2d30-3033-3133-383188f79eec 28.083333 11.500000 111.166667 0.332879 0.400927 7 0.779968 168.5
10 T.Hill KC 32013030-2d30-3033-3330-3430e890f1ff 18.111111 8.000000 110.444444 0.220912 0.383002 8 0.599469 163.0
11 S.Diggs BUF 32013030-2d30-3033-3135-383848cdfbb6 18.033333 10.111111 107.666667 0.303287 0.384180 5 0.723857 162.3

I said in the beginning of this post that we'll also be adding player headshots, team logos, and some styling to make our DataFrame a bit more applealing to the eye.

We can quickly load in roster data and team logo data using the two functions below from the nfl module.

team_abbr team_name team_id team_nick team_color team_color2 team_color3 team_color4 team_logo_wikipedia team_logo_espn
0 ARI Arizona Cardinals 3800 Cardinals #97233f #000000 #ffb612 #a5acaf https://upload.wikimedia.org/wikipedia/en/thum... https://a.espncdn.com/i/teamlogos/nfl/500/ari.png
teamPlayers.gsisId teamPlayers.headshot_url
53064 00-0034422 http://static.nfl.com/static/content/public/st...

First, let's merge player headshots. Unfortunately, player headshot data is only available for players who played in 2019. We are going to come up with an alternative solution for rookies.

Before we actually merge the data, we have to convert the receiver_player_id's to a new ID format called gsis_id which will allow us a common column between the tables. I've added a utility function within nflfastpy that allows us to do this.

Receiver Team receiver_player_id Avg. Raw Fpts Avg. Targets Avg. Air Yards Avg. Target Share Avg. Air Yards Share Redzone Looks Avg. WOPR total_fpts gsis_id teamPlayers.headshot_url
0 T.Kelce KC 32013030-2d30-3033-3035-3036654ef292 18.988889 8.888889 67.000000 0.233923 0.230771 7 0.512424 170.9 00-0030506 http://static.nfl.com/static/content/public/st...
1 D.Metcalf SEA 32013030-2d30-3033-3536-343097915ff1 21.225000 8.500000 123.625000 0.244524 0.414175 10 0.656708 169.8 00-0035640 http://static.nfl.com/static/content/public/st...
2 D.Adams GB 32013030-2d30-3033-3133-383188f79eec 28.083333 11.500000 111.166667 0.332879 0.400927 7 0.779968 168.5 00-0031381 http://static.nfl.com/static/content/public/st...
3 T.Hill KC 32013030-2d30-3033-3330-3430e890f1ff 18.111111 8.000000 110.444444 0.220912 0.383002 8 0.599469 163.0 00-0033040 http://static.nfl.com/static/content/public/st...
4 S.Diggs BUF 32013030-2d30-3033-3135-383848cdfbb6 18.033333 10.111111 107.666667 0.303287 0.384180 5 0.723857 162.3 00-0031588 http://static.nfl.com/static/content/public/st...

Let's write a function that says if a player's headshot does not exist (they are a rookie), then format their headshot as a default player headshot via the URL within the function below.

We are going to be converting these headshots to HTML images.

Receiver Team receiver_player_id Avg. Raw Fpts Avg. Targets Avg. Air Yards Avg. Target Share Avg. Air Yards Share Redzone Looks Avg. WOPR total_fpts gsis_id teamPlayers.headshot_url Headshot
0 T.Kelce KC 32013030-2d30-3033-3035-3036654ef292 18.988889 8.888889 67.000000 0.233923 0.230771 7 0.512424 170.9 00-0030506 http://static.nfl.com/static/content/public/st... <img src="http://static.nfl.com/static/content...
1 D.Metcalf SEA 32013030-2d30-3033-3536-343097915ff1 21.225000 8.500000 123.625000 0.244524 0.414175 10 0.656708 169.8 00-0035640 http://static.nfl.com/static/content/public/st... <img src="http://static.nfl.com/static/content...
2 D.Adams GB 32013030-2d30-3033-3133-383188f79eec 28.083333 11.500000 111.166667 0.332879 0.400927 7 0.779968 168.5 00-0031381 http://static.nfl.com/static/content/public/st... <img src="http://static.nfl.com/static/content...
3 T.Hill KC 32013030-2d30-3033-3330-3430e890f1ff 18.111111 8.000000 110.444444 0.220912 0.383002 8 0.599469 163.0 00-0033040 http://static.nfl.com/static/content/public/st... <img src="http://static.nfl.com/static/content...
4 S.Diggs BUF 32013030-2d30-3033-3135-383848cdfbb6 18.033333 10.111111 107.666667 0.303287 0.384180 5 0.723857 162.3 00-0031588 http://static.nfl.com/static/content/public/st... <img src="http://static.nfl.com/static/content...

Let's do the same thing with team logos. Luckily, all team logos are available, so we won't have to default to an alternative solution for missing values like we did above.

Receiver Team receiver_player_id Avg. Raw Fpts Avg. Targets Avg. Air Yards Avg. Target Share Avg. Air Yards Share Redzone Looks Avg. WOPR ... Headshot team_name team_id team_nick team_color team_color2 team_color3 team_color4 team_logo_wikipedia team_logo_espn
0 T.Kelce <img src="https://upload.wikimedia.org/wikiped... 32013030-2d30-3033-3035-3036654ef292 18.988889 8.888889 67.000000 0.233923 0.230771 7 0.512424 ... <img src="http://static.nfl.com/static/content... Kansas City Chiefs 2310 Chiefs #e31837 #ffb612 #000000 #e31837 https://upload.wikimedia.org/wikipedia/en/thum... https://a.espncdn.com/i/teamlogos/nfl/500/kc.png
1 T.Hill <img src="https://upload.wikimedia.org/wikiped... 32013030-2d30-3033-3330-3430e890f1ff 18.111111 8.000000 110.444444 0.220912 0.383002 8 0.599469 ... <img src="http://static.nfl.com/static/content... Kansas City Chiefs 2310 Chiefs #e31837 #ffb612 #000000 #e31837 https://upload.wikimedia.org/wikipedia/en/thum... https://a.espncdn.com/i/teamlogos/nfl/500/kc.png
2 M.Hardman <img src="https://upload.wikimedia.org/wikiped... 32013030-2d30-3033-3531-343087bd8daf 9.188889 3.777778 24.222222 0.097382 0.083855 0 0.204772 ... <img src="http://static.nfl.com/static/content... Kansas City Chiefs 2310 Chiefs #e31837 #ffb612 #000000 #e31837 https://upload.wikimedia.org/wikipedia/en/thum... https://a.espncdn.com/i/teamlogos/nfl/500/kc.png
3 C.Edwards-Helaire <img src="https://upload.wikimedia.org/wikiped... 32013030-2d30-3033-3633-3630b28b4868 6.266667 4.777778 1.888889 0.130025 0.001534 0 0.196112 ... <img src="https://sportsfly.cbsistatic.com/bun... Kansas City Chiefs 2310 Chiefs #e31837 #ffb612 #000000 #e31837 https://upload.wikimedia.org/wikipedia/en/thum... https://a.espncdn.com/i/teamlogos/nfl/500/kc.png
4 S.Watkins <img src="https://upload.wikimedia.org/wikiped... 32013030-2d30-3033-3133-32353107e672 11.040000 5.800000 42.200000 0.165896 0.199569 2 0.388542 ... <img src="http://static.nfl.com/static/content... Kansas City Chiefs 2310 Chiefs #e31837 #ffb612 #000000 #e31837 https://upload.wikimedia.org/wikipedia/en/thum... https://a.espncdn.com/i/teamlogos/nfl/500/kc.png

5 rows × 23 columns

Let's filter out some columns since our DataFrame got pretty messy when we started merging everything toghther.

Receiver Team receiver_player_id Avg. Raw Fpts Avg. Targets Avg. Air Yards Avg. Target Share Avg. Air Yards Share Redzone Looks Avg. WOPR total_fpts Headshot
0 T.Kelce <img src="https://upload.wikimedia.org/wikiped... 32013030-2d30-3033-3035-3036654ef292 18.988889 8.888889 67.000000 0.233923 0.230771 7 0.512424 170.9 <img src="http://static.nfl.com/static/content...
1 T.Hill <img src="https://upload.wikimedia.org/wikiped... 32013030-2d30-3033-3330-3430e890f1ff 18.111111 8.000000 110.444444 0.220912 0.383002 8 0.599469 163.0 <img src="http://static.nfl.com/static/content...
2 M.Hardman <img src="https://upload.wikimedia.org/wikiped... 32013030-2d30-3033-3531-343087bd8daf 9.188889 3.777778 24.222222 0.097382 0.083855 0 0.204772 82.7 <img src="http://static.nfl.com/static/content...
3 C.Edwards-Helaire <img src="https://upload.wikimedia.org/wikiped... 32013030-2d30-3033-3633-3630b28b4868 6.266667 4.777778 1.888889 0.130025 0.001534 0 0.196112 56.4 <img src="https://sportsfly.cbsistatic.com/bun...
4 S.Watkins <img src="https://upload.wikimedia.org/wikiped... 32013030-2d30-3033-3133-32353107e672 11.040000 5.800000 42.200000 0.165896 0.199569 2 0.388542 55.2 <img src="http://static.nfl.com/static/content...

Finally, our DataFrame. Here, we're creating a copy of our original DataFrame so we can alter it as a copy rather than a view. We then can use Pandas DataFrame.styling module to add dynamic styling. Here, we're highlighing min values with a red color, and adding a background gradient to both targets and target share.

Receiver Team Avg. Raw Fpts Avg. Targets Avg. Air Yards Avg. Target Share Avg. Air Yards Share Redzone Looks Avg. WOPR
T.Kelce 18.988889 8.888889 67.000000 0.233923 0.230771 7 0.512424
D.Metcalf 21.225000 8.500000 123.625000 0.244524 0.414175 10 0.656708
D.Adams 28.083333 11.500000 111.166667 0.332879 0.400927 7 0.779968
T.Hill 18.111111 8.000000 110.444444 0.220912 0.383002 8 0.599469
S.Diggs 18.033333 10.111111 107.666667 0.303287 0.384180 5 0.723857
T.Lockett 19.562500 8.750000 92.000000 0.236870 0.287703 9 0.556698
D.Hopkins 18.925000 9.500000 81.875000 0.287161 0.296892 3 0.638566
K.Allen 18.887500 10.875000 88.250000 0.297924 0.300861 6 0.657488
A.Robinson 16.244444 9.555556 99.555556 0.240884 0.300222 6 0.571482
C.Ridley 18.112500 8.750000 124.375000 0.224673 0.355352 10 0.585755
R.Anderson 15.700000 9.000000 88.333333 0.270606 0.384727 4 0.675218
T.McLaurin 17.150000 9.750000 97.000000 0.292507 0.496391 4 0.786234
A.Cooper 15.166667 9.222222 78.888889 0.221353 0.247988 4 0.505621
W.Fuller 18.714286 7.428571 95.000000 0.224549 0.322839 4 0.562812
T.Boyd 16.300000 8.500000 73.000000 0.213146 0.226127 5 0.478007
A.Thielen 15.900000 7.500000 97.875000 0.301737 0.414823 10 0.742982
J.Jones 17.828571 8.000000 89.000000 0.208600 0.275025 7 0.505418
C.Lamb 13.500000 7.555556 77.111111 0.188760 0.253639 6 0.460687
M.Evans 13.300000 6.000000 65.333333 0.157457 0.192331 8 0.370817
D.Moore 13.000000 6.888889 82.555556 0.215062 0.365288 4 0.578295
A.Brown 16.542857 7.428571 72.857143 0.252116 0.323225 4 0.604432
J.Jefferson 14.362500 5.625000 70.500000 0.221461 0.350219 1 0.577344
D.Waller 14.175000 9.000000 50.125000 0.292961 0.203676 6 0.582015
C.Kupp 14.137500 8.875000 64.250000 0.250515 0.242250 3 0.545348
J.Smith-Schuster 14.137500 7.250000 41.375000 0.195525 0.162838 4 0.407274

And that's it! Thank you for reading. If you have any questions regarding the code, feel free to email me at [email protected]