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

Shameless Plug Section

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.

Value over replacement

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.

Python code and results

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!