Learn how to use Python to find the most valuable players for the 2020 season.
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 16 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're going to try to find which players have been the most valuable so far through 14 weeks of the NFL season. For more or less everyone, this is the end of the "regular season" for fantasy football, so it seems like a good time to check out which players have been the most valuable for fantasy teams this season.
How we do we quantify value, though? If you've followed my previous posts, you're probably familiar with the concept of value over replacement player (we previously used VOR to rank players for the 2020 draft).
Value over replacement player is a number that quantifies how much value, or points, a player gets you over a typical replacement player you could start at his position.
Let's use an example with Travis Kelce to illustrate VOR. This season Kelce consistently put up 20 points a game, while the rest of the tight end market was a barren wasteland. If you had Kelce, and he was out one week, your next best option (let's assume you don't have a TE on your bench) would have been to go to the waivers and grab the next best TE. Pain. As anyone who drafted Fant, Ertz, or Goedert this year knows, streaming tight ends this year was definitively not a good strategy
Let's say the next best tight end, maybe it was Logan Thomas, got you 6 points. If Logan Thomas got you 6 points, and Kelce consistently gets you 20, then Kelce's VOR for that week is 14 (20 - 6). In other words, if you had started Kelce that week, you could have expected a solid 20 points. But, in this example, he was out that week so you had to start the next man up, Logan Thomas, and he got you 6 points. In essence, your team lost 14 points because Kelce was out.
We're going to be using this concept of "next man up" to quantify value for each player. Essentially, a player's value is how many points they would score you above a typical replacement player, where a typical replacement player is the next best guy (at the same position) sitting on the waivers. You'll see that this measure of value puts RBs, WRs, and in some cases, TEs at that top. QB's dont start to show up on these rankings until 25 or so. If you've been playing fantasy a while, this confirms what you already know. Top QBs aren't as valuable as WRs, RBs, and top TEs, because worst case scenario you can always go to the waivers and pick up a decent stream. In the example above, it's very easy to see a situation where you have Kelce, he's out one week, and you lose 14 points at tight end by starting a waiver wire TE. It's a lot harder to imagine how this could happen at QB, even if you have someone like Murray or Mahomes.
This is obviously an estimate of fantasy football value, and not a perfect model. FOr instance, it assumes that the next man up is available for pickup, and isn't sitting on someone else's lineup. But in general, the results are more or less reasonable and useful when making comparisons between positions. QBs, on average, score more points than all other positions, but we know QBs aren't the most valuable assets in fantasy football. This indicates that looking at points scored isn't the best measure of fantasy football value. Instead, value needs to be looked at with respect to each player's position.
That's enough theory. On to coding now. Import your libraries in the first cell of your Google Colab notebook. For those who are new to FFDP and writing code, Google Colab is an interactive, browser-based notebook environment that let's you run a variant of the Python programming language known as IPython, or interactive Python. IPython is used heavily in data science circles, and as such, comes with many of the Python data science libraries already plugged in.
In this post, we're going to be grabbing 2020 data from PFR by quickly scraping the page for an HTML table and converting it to a DataFrame using pandas' read_html function.
After we import our libraries, we're going to scrape the 2020 fantasy stats page from PFR.
Player | Tm | FantPos | Rec | FantPt | PPRFantPt | |
---|---|---|---|---|---|---|
0 | Dalvin Cook | MIN | RB | 37 | 257 | 294 |
1 | Derrick Henry | TEN | RB | 17 | 246 | 263 |
2 | Tyreek Hill | KAN | WR | 77 | 224 | 301 |
3 | Alvin Kamara | NOR | RB | 77 | 226 | 303 |
4 | Travis Kelce | KAN | TE | 90 | 177 | 267 |
I encourage you to visit the URL saved to the variable url and check out the format and structure of the page/data we are scraping. As you can see here, the data already contains a column for fantasy points, but the fantasy points numbers are in standard format. If you want to do this analysis for half PPR or standard, simply don't include the last line where we add receptions in.
The below code is where we calculate value for each player. As you can see here, we have a dictionary with positions as keys and "cutoff points" as values. In a 12 man league, there's going to be 24 startable RBs available at any point (for simplicity, we're only considering roster spots where a player at a particular position must be started, not FLEX). This is a 2RB, 2WR, 1TE, 1QB league. The next best RB you would have to start, given one of your starting RB's is out, is RB #25 (again, you can probably see how this value model isn't a perfect representation of reality. The #25 RB isn't likely available to you, and may be on another opponents bench. It's important to understand that VOR is an estimate, and a player's value may change based on your individual lineup and league.
Circling back to the code below, we split our data up on position, sort each position DataFrame in descending order, and find the cutoff player at each position. Once we find the #25 RB, #13 QB, #25 WR, and #13 TE, we then find that player's fantasy output for the year and append that to our replacement_values dictionary. This dictionary contains our replacement values for each position (an estimate of the amount of points we could expect to receive given one of our starting players at that position was out).
We then do some weird data wrangling to get our replacement_values dict as a DataFrame and in the right position to merge, calculate a column called PPR_Value, and sort the table by this column in descending order.
Player | Tm | FantPos | Rec | FantPt | PPRFantPt | Replacement | PPR_Value | |
---|---|---|---|---|---|---|---|---|
0 | Travis Kelce | KAN | TE | 90 | 177 | 267 | 72 | 195 |
1 | Alvin Kamara | NOR | RB | 77 | 226 | 303 | 108 | 195 |
2 | Tyreek Hill | KAN | WR | 77 | 224 | 301 | 109 | 192 |
3 | Dalvin Cook | MIN | RB | 37 | 257 | 294 | 108 | 186 |
4 | Davante Adams | GNB | WR | 91 | 196 | 287 | 109 | 178 |
5 | Derrick Henry | TEN | RB | 17 | 246 | 263 | 108 | 155 |
6 | Stefon Diggs | BUF | WR | 100 | 147 | 247 | 109 | 138 |
7 | D.K. Metcalf | SEA | WR | 69 | 176 | 245 | 109 | 136 |
8 | Keenan Allen | LAC | WR | 99 | 144 | 243 | 109 | 134 |
9 | Darren Waller | LVR | TE | 84 | 122 | 206 | 72 | 134 |
10 | DeAndre Hopkins | ARI | WR | 94 | 144 | 238 | 109 | 129 |
11 | James Robinson | JAX | RB | 46 | 190 | 236 | 108 | 128 |
12 | Allen Robinson | CHI | WR | 86 | 139 | 225 | 109 | 116 |
13 | Calvin Ridley | ATL | WR | 67 | 154 | 221 | 109 | 112 |
14 | Justin Jefferson | MIN | WR | 65 | 154 | 219 | 109 | 110 |
15 | Tyler Lockett | SEA | WR | 81 | 137 | 218 | 109 | 109 |
16 | Patrick Mahomes | KAN | QB | 0 | 329 | 329 | 222 | 107 |
17 | Kyler Murray | ARI | QB | 0 | 326 | 326 | 222 | 104 |
18 | Adam Thielen | MIN | WR | 60 | 152 | 212 | 109 | 103 |
19 | Amari Cooper | DAL | WR | 80 | 128 | 208 | 109 | 99 |
20 | Robert Woods | LAR | WR | 76 | 131 | 207 | 109 | 98 |
21 | Aaron Jones | GNB | RB | 38 | 165 | 203 | 108 | 95 |
22 | Aaron Rodgers | GNB | QB | 1 | 313 | 314 | 222 | 92 |
23 | T.J. Hockenson | DET | TE | 58 | 104 | 162 | 72 | 90 |
24 | Russell Wilson | SEA | QB | 0 | 310 | 310 | 222 | 88 |
25 | Robby Anderson | CAR | WR | 83 | 113 | 196 | 109 | 87 |
26 | Josh Allen | BUF | QB | 1 | 306 | 307 | 222 | 85 |
27 | Kareem Hunt | CLE | RB | 31 | 162 | 193 | 108 | 85 |
28 | A.J. Brown | TEN | WR | 51 | 142 | 193 | 109 | 84 |
29 | Terry McLaurin | WAS | WR | 73 | 119 | 192 | 109 | 83 |
30 | Ezekiel Elliott | DAL | RB | 45 | 146 | 191 | 108 | 83 |
31 | Mike Davis | CAR | RB | 57 | 133 | 190 | 108 | 82 |
32 | Robert Tonyan | GNB | TE | 46 | 107 | 153 | 72 | 81 |
33 | Tyler Boyd | CIN | WR | 78 | 112 | 190 | 109 | 81 |
34 | Will Fuller | HOU | WR | 53 | 136 | 189 | 109 | 80 |
35 | JuJu Smith-Schuster | PIT | WR | 79 | 110 | 189 | 109 | 80 |
36 | David Montgomery | CHI | RB | 42 | 145 | 187 | 108 | 79 |
37 | Cooper Kupp | LAR | WR | 79 | 106 | 185 | 109 | 76 |
38 | Cole Beasley | BUF | WR | 71 | 113 | 184 | 109 | 75 |
39 | Antonio Gibson | WAS | RB | 32 | 151 | 183 | 108 | 75 |
40 | Josh Jacobs | LVR | RB | 30 | 153 | 183 | 108 | 75 |
41 | Mike Evans | TAM | WR | 51 | 133 | 184 | 109 | 75 |
42 | Deshaun Watson | HOU | QB | 0 | 293 | 293 | 222 | 71 |
43 | Mike Gesicki | MIA | TE | 44 | 96 | 140 | 72 | 68 |
44 | Chase Claypool | PIT | WR | 50 | 127 | 177 | 109 | 68 |
We can see here that Travis Kelce was the most valuable fantasy player all season. I knew Kelce was near the top already while writing this post, which is why I used Kelce as an example. In my opinion, he gets my vote for fantasy MVP and should be a first round pick next year. Darren Waller at #9 is unsurprising too. He hasn't been as solid as Kelce, but he does sometimes put the occasional 12-15 catch game that will win you your week, given options at TE.
Before we end off this post, another interesting thing we can do is group by team and find the teams that provided the most fantasy value this season.
Tm | PPR_Value | |
---|---|---|
15 | KAN | 553 |
11 | GNB | 446 |
20 | MIN | 399 |
25 | SEA | 383 |
28 | TEN | 377 |
0 | ARI | 344 |
3 | BUF | 298 |
24 | PIT | 289 |
8 | DAL | 284 |
4 | CAR | 283 |
All of the results here make sense. Mahomes, Kelce, and Hill probably account for 95% of that 553 number. What's crazy is that, given ADP at the start of the season, it was totally possible to draft Tyreek Hill at the end of the first, Kelce at the turn, and Mahomes in the third if no one in your league was willing to draft QB early.
That's it for this post. Thank you for reading, you guys are awesome!