Learn how to use Python to calculate fantasy football opportunity metrics.
If you have any questions about the code here, feel free to reach out to me on Twitter or on Reddit.
If you like Fantasy Football and have an interest in learning how to code, check out our Ultimate Guide on Learning Python with Fantasy Football Online Course. Here is a link to purchase for 15% off. The course includes 15 chapters of material, 14 hours of video, hundreds of data sets, lifetime updates, and a Slack channel invite to join the Fantasy Football with Python community.
In this part of the intermediate series, we are going to be talking about and introducing a new concept on the blog called WOPR, which stands for weighted opportunity rating. The goal of this post will be to find a way to measure receiver "opportunity" and find those player's who are getting the most "opportunity" on their respective offenses.
Measuring opportunity is vital in optimizing your roster for fantasy football. Simply put, if your players are not getting opportunities, then they are not going to score you points, and watching redzone on Sunday is going to be frustrating as hell. The key to winning consistently at fantasy football is to prioritize selecting, trading for, and picking up players who are consistently involved in their respective offenses. But what does being involved mean?
Air yards is the best measure we have in fantasy football of intent. Air yards are how far a ball went during a particular throw, regardless if it was caught or not. A player's total air yards for a given week is his average depth of target times times his number of targets that week. Later in this post, we'll look at the relationship between air yards and fantasy football performance using scipy.stats and matplotlib.
How can air yards help us measure opportunity? Well, a naive measure of measuring opportunity in fantasy football is to simply look at raw targets or targets per game. This approach simply isn't adequete enough when you consider a target near the line of scrimmage doesn't have the same amount of opportunity as a 50 yard bomb that could potentially be a 70 yard play. Adding air yards to the mix can help us account for this and come up with a more nuanced measure of fantasy football opportunity.
WOPR is defined as a weighted average of a player's target market share (their individual targets / team total targets for a particular week) and a player's air yard market share (an individual's air yards / team total air yards for a particular week).
WOPR = 1.5 * target_share + 0.7 * air_yards_share
In this post, we are going to be pulling NFL play by play data for the past 5 weeks and finding each player's WOPR for each week. We are going to also be throwing some matplotlib in to the mix and visualizing the relationship between air yards, yards after catch, and WOPR and receiving fantasy football points.
First things first, load up either a Google Colab notebook or jupyter ipynb notebook and import the libraries we'll need in the first cell.
Next, let's load in 2020 play by play data via nflfastR.
The next thing we need to do is to filter our DataFrame to only include pass plays and grab the relevant columns to our analysis. We're going to calculate the receiving fantasy points scored on each play, then groupby player id, player name, team, and week and consolidate the rows in to a weekly receiving stat line for each player.
From there, we'll use groupby again to find team targets and team team air yards, merge that with our weekly stat line DataFrame, and then calculate WOPR.
receiver_player_id | receiver_player_name | posteam | week | target_ind | complete_pass | yards_after_catch | yards_gained | touchdown | air_yards_ind | rec_fp | air_yards_team | target_team | weekly_wopr | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 32013030-2d30-3032-3231-32373ce51f62 | J.Witten | LV | 1 | 1.0 | 1.0 | 0.0 | 2.0 | 0.0 | 2.0 | 1.2 | 161.0 | 28.0 | 0.062267 |
1 | 32013030-2d30-3032-3231-32373ce51f62 | J.Witten | LV | 2 | 1.0 | 1.0 | 0.0 | 3.0 | 0.0 | 3.0 | 1.3 | 239.0 | 34.0 | 0.052904 |
2 | 32013030-2d30-3032-3231-32373ce51f62 | J.Witten | LV | 4 | 2.0 | 2.0 | 0.0 | 18.0 | 1.0 | 18.0 | 9.8 | 267.0 | 43.0 | 0.116958 |
3 | 32013030-2d30-3032-3231-32373ce51f62 | J.Witten | LV | 5 | 2.0 | 2.0 | 7.0 | 6.0 | 0.0 | -1.0 | 2.6 | 271.0 | 31.0 | 0.094191 |
4 | 32013030-2d30-3032-3239-323176c2a1fa | L.Fitzgerald | ARI | 1 | 5.0 | 4.0 | 23.0 | 34.0 | 0.0 | 15.0 | 7.4 | 192.0 | 37.0 | 0.257390 |
Next, let's look at the relationship between air yards, yards after catch, and WOPR and receiving fantasy points. We're going to be using the stats package to find the R^2 and using that as our plot titles.
As we can see here, WOPR was more predictive of fantasy football performance (0.746 R^2) than both yards after catch and air yards.
Finally, let's group by player and sum up their weekly WOPR to get a season-long WOPR number.
receiver_player_id | receiver_player_name | posteam | season_wopr | |
---|---|---|---|---|
41 | 32013030-2d30-3033-3030-3335960ad201 | A.Thielen | MIN | 4.284696 |
344 | 32013030-2d30-3033-3536-353952c3dc5d | T.McLaurin | WAS | 3.717295 |
72 | 32013030-2d30-3033-3132-3335cebc9f07 | O.Beckham | CLE | 3.512059 |
345 | 32013030-2d30-3033-3536-36327e9d96c5 | M.Brown | BAL | 3.493854 |
61 | 32013030-2d30-3033-3035-3634b926c47f | D.Hopkins | ARI | 3.454431 |
48 | 32013030-2d30-3033-3032-3739a5751069 | K.Allen | LAC | 3.420057 |
341 | 32013030-2d30-3033-3536-343097915ff1 | D.Metcalf | SEA | 3.400057 |
86 | 32013030-2d30-3033-3134-3238120ea790 | A.Robinson | CHI | 3.389711 |
289 | 32013030-2d30-3033-3438-333761eb5105 | C.Ridley | ATL | 3.373140 |
333 | 32013030-2d30-3033-3535-33356c42f49d | D.Slayton | NYG | 3.336261 |
126 | 32013030-2d30-3033-3236-3838f52d40a0 | R.Anderson | CAR | 3.298369 |
93 | 32013030-2d30-3033-3135-383848cdfbb6 | S.Diggs | BUF | 3.172083 |
286 | 32013030-2d30-3033-3438-3237a7c47510 | D.Moore | CAR | 2.950833 |
96 | 32013030-2d30-3033-3136-3130db0aa3c4 | D.Waller | LV | 2.870532 |
13 | 32013030-2d30-3032-3731-35304d0e9eb8 | J.Edelman | NE | 2.771220 |
142 | 32013030-2d30-3033-3330-3430e890f1ff | T.Hill | KC | 2.743511 |
88 | 32013030-2d30-3033-3135-3434ca99a9bc | A.Cooper | DAL | 2.726952 |
36 | 32013030-2d30-3032-3936-30385788c9a5 | T.Hilton | IND | 2.671263 |
210 | 32013030-2d30-3033-3339-3038a825a9da | C.Kupp | LA | 2.652673 |
115 | 32013030-2d30-3033-3232-31312f766863 | T.Lockett | SEA | 2.626906 |
58 | 32013030-2d30-3033-3035-3036654ef292 | T.Kelce | KC | 2.608714 |
15 | 32013030-2d30-3032-3736-383548509814 | E.Sanders | NO | 2.596778 |
274 | 32013030-2d30-3033-3437-353395a63095 | M.Andrews | BAL | 2.516684 |
73 | 32013030-2d30-3033-3132-333658a3c4ba | B.Cooks | HOU | 2.436638 |
152 | 32013030-2d30-3033-3331-3237332aee05 | W.Fuller | HOU | 2.394942 |
382 | 32013030-2d30-3033-3633-32324e92bd12 | J.Jefferson | MIN | 2.344751 |
140 | 32013030-2d30-3033-3330-303984f9bb0c | T.Boyd | CIN | 2.329129 |
145 | 32013030-2d30-3033-3330-393046e5de90 | H.Henry | LAC | 2.295694 |
51 | 32013030-2d30-3033-3034-333120799932 | R.Woods | LA | 2.276193 |
89 | 32013030-2d30-3033-3135-3437b94ee00a | D.Parker | MIA | 2.256515 |
And there you have it, our top 30 receivers this year in terms of WOPR. These are the players that are receiving the most fantasy football opportunity this year (based on how we defined opportunity).
Some players here are unsurprising. Hollywood Brown receives a ton of air yards and probably a decent market share of his teams targets considering the Ravens don't have many receiving options.
One player that is a bit surprising and that has shown up two weeks in a row near the top of the list is Darius Slayton. Given last week's performance, it appears that Darius Slayton may be primed to finish as a WR3 this year, and maybe even WR2 if he becomes a bit more consistent.
Another player that has shown up 2 weeks in a row high up on this list is DJ Moore. He finally had his "breakout" game last Sunday, recording about 19 PPR fantasy points. The bulk of his fantasy points came on a 50+ yard touchdown that went 4 air yards.
desc | air_yards | yards_gained | touchdown | |
---|---|---|---|---|
11907 | (2:40) (Shotgun) 5-T.Bridgewater pass short left to 12-D.Moore for 57 yards, TOUCHDOWN. | 4.0 | 57.0 | 1.0 |
DJ Moore's usage is concerning for sure. He's getting a ton of air yards, but those high opportunity passes haven't really been catchable thus far. It looks like it might be the Robby Anderson show in CAR from now on.
Thanks for reading! Hopefully you found this analysis useful to your fantasy football team and more importantly, you were able to improve your pandas/python/matplotlib skills.