I’m playing in a fantasy football league with some coworkers this year. I have always liked all kinds of sports, but it’s a little bit challenging to follow NFL from Europe. There’s not too many stations that actually show it (except for the Super Bowl) and even if they do, the time difference make it difficult to watch. I still followed major news and managed to catch a game every now and then, but I was very far from expert. Basically, only knew big names and the most popular teams. It did get a little bit better after I moved here, but I’m still very ignorant. My only chance not to embarrass myself completely was to follow the steps of Mark Watney and ‘science the shit out of this’.
If you’re not familiar with the concept of fantasy football, Wikipedia has a fairly detailed description , but a TLDR; version is this:
- each participant drafts his team (of real NFL players),
- every week you’re awarded points based on performance of your players. Exact scoring rules differ between leagues, but some key numbers from our league: touchdown is worth 6 (receiving/rushing) or 4 (for a quarterback) pts, every rushing/receiving yard is 0.1 pts and so on. 15pts/week is a decent score, anything over 20 is really good and 30+ is amazing (this shifts a little bit in PPR formats).
- every week you only compete against 1 other team. Team that gets more points (sum of points from all players) wins. In our league, 100pts is a fairly decent score, 120+ will get you a guaranteed win most weeks
- every week you’re allowed to replace your players with players that are free (not playing for any other team). You also have to decide who plays and who sits (full team includes substitute players).
There’s plenty of draft strategies, but I feel it’s still the most “random” point of the season. You can only go by past data and projections, but it’s hard to say how it’ll translate to current seasons. I’m not going to talk about draft here. My system comes into play after few weeks after we’ve collected some samples from the new season. We want to see who to grab from the waiver wire and who to sit/play. Again, it’s a matter of personal preferences, but I’m fairly risk averse, so I was mostly interested in finding most “consistently decent” players. For example, consider these 2 stat lines:
- player A: 7, 21, 5, 16 (mean: 12.25, std dev: 7.54)
- player B: 11.5, 10.5, 12, 11.5 (mean: 11.375, std dev: 0.63)
Player A gets more points on average (12.25 vs 11.375), but I’d take player B. He’s way more reliable. I can plan my team better, I know (roughly) what to expect. Player A is the type that you sit, he gets 18, you let him play next week and he gets 5. My quest was to find players who’re not necessarily the most flash and bring the most points, but to find who’s the most consistent and should bring at least X pts every week. Before we start, a small disclaimer: my methodology here has almost nothing to do with actual science. Sample sizes are laughably small and fantasy points distribution definitely isn’t guaranteed to be normal. I basically tweaked various factors until I got results that made sense.
My idea was simple: grab data from some site, calculate mean/variance/std deviation for every player, reject outliers, recalculate, compute the “floor” (mean – std dev * some_weight). Our “floor” is basically telling us the minimum number of points we can reasonably expect. As mentioned, I’m fairly conservative, so I actually had an option to only reject “positive outliers” (that is, outliers that are greater than mean + std dev, we still keep samples where player underperformed). In our example, it’d keep all the samples for players B, but reject 21 points for player A. After rejection, player A’s average drops to 9.33 (new std dev is 5.86).
Spent some time trying to find the best source of stats and opted for FF Today. They update their stats quite often and format is fairly easy to parse. I couldn’t find an aggregate version, so I simply visit category stats and then traverse all the top players pages. My go-to combo for simple web scraping is Python+Beautiful Soup. ~100 lines of code later I had the first version of my script ready. My first hit was Stefon Diggs, but you could argue he wasn’t a real sleeper after Week 7 (script actually pointed him out after W6, but it was also due to super small sample size…). I got him from the waiver wire, but didn’t trust him enough to let him play (not enough data) and that turned out to be a good decision (his last two games were not that great). Week 10 brought a more serious try — I had to find a new kicker (my main kicker Matt Bryant didn’t play that week). Based on expert prediction I should have gone with Greg Zuerlein or Caleb Sturgis (these were the highest ranked kickers that were still available in my league). However, the script had the following to say:
1. Connor Barth 11.1004809472
2. Nick Folk 5.47483805032
3. Chandler Catanzaro 5.47186593476
4. Stephen Gostkowski 5.46112639465
5. Brandon McManus 5.24049168899
6. Dan Bailey 4.91128425904
7. Dustin Hopkins 4.61254139118
8. Steve Hauschka 4.60733909541
9. Blair Walsh 4.15430405875
10. Josh Lambo 4.0
12. Greg Zuerlein 3.82037546488
14. Caleb Sturgis 2.69765682193
Most of the highest ranked kickers were not available/injured, but Dustin Hopkins was still for grabs. As you can see, he’s expected to bring more points than both Zuerlein and Sturgis. Experts ranked him around 20th place this week, so not much confidence. I’m not entirely sure why tbh, I think it’s mostly no one really cares much about kickers. He’s only missed one FG this season (~92%). He plays for a mediocre team, but that factor is actually ‘encoded’ in the score above, he was playing for the same team when earning fantasy points so far. I decided to go with Hopkins and it ended way better than I could expect — he got me 17 pts this week. Now, if I want to be fair, I have to admit this was completely unexpected, based on his history his expected max score was around 12, but I’ll take it. It is a “positive outlier” I mentioned before, though so my algorithm will reject it when evaluating it in the future.
Hopefully it’s obvious, but I’d like to stress that this is just pure data analysis. Algorithm cares only about fantasy points. It has no idea about matchups, injuries and team strategy. In theory, fantasy points encode all these and ideally we’re looking for players “immune” to these factors, but some domain knowledge is recommended. For example, if you run the script with my default settings (stddevweight=1, rejectonlyposoutliers), top RBs look like:
python.exe nfl_crawler2.py –pos rb –rejectonlyposoutliers
1. Jamaal Charles — 10.66
2. Karlos Williams — 9.93
3. Mark Ingram — 9.01
4. LeSean McCoy — 8.83
5. Todd Gurley — 8.26
6. Devonta Freeman — 8.19
It can surprising to see Gurley and Freeman so low (and Miller is nowhere to be found), but it makes more sense if you remember Gurley had a very short outing in his first game (1.4pts) and Freeman’s actually fairly volatile (still brilliant) and that affects his deviation (he also had a 4.7pts game). Miller is not even in the top 10 because he wasn’t getting many touches with the old coach. If we run the same script with different options (reject all outliers, not only positives and only consider last 6 games, results are a little bit different):
python.exe nfl_crawler2.py –pos rb –lastn 6
1. Todd Gurley — 14.91
2. Devonta Freeman — 13.46
3. Chris Ivory — 11.79
4. Lamar Miller — 11.15
5. LeSean McCoy — 10.96
6. Adrian Peterson — 10.54
7. Karlos Williams — 9.93
Williams is probably the biggest surprise here, but he’s been posting great numbers so far (if not injured). His worst game was 9.7pts and while his ceiling might be lower than other guys he’s actually very consistent (dev of 4.5). His main problem is named McCoy (#5) who’s Buffalo’s RB1.
Without further ado, here’s a list for wide receivers:
python.exe nfl_crawler2.py –pos wr –rejectonlyposoutliers
1. Eric Decker — 9.71
2. Brandon Marshall — 8.08
3. DeAndre Hopkins — 7.53
4. Allen Hurns — 7.06
5. Larry Fitzgerald — 6.96
6. Jarvis Landry — 6.58
7. Julio Jones — 6.43
8. Allen Robinson — 6.30
9. Julian Edelman — 6.04
10. Demaryius Thomas — 5.43
11. Odell Beckham Jr. — 5.41
12. Keenan Allen — 5.15
13. Stefon Diggs — 5.10
14. Calvin Johnson — 5.07
15. Amari Cooper — 4.80
16. Rishard Matthews — 4.72
17. Alshon Jeffery — 4.66
18. T.Y. Hilton — 4.35
19. A.J. Green — 4.20
20. Travis Benjamin — 4.10
21. Mike Evans — 3.76
22. Antonio Brown — 3.74
Again, it can be a little bit surprising (especially Brown at 22, but he really suffered while Big Ben was away), but that’s an ultra conservative setting, it’s easy to adjust the script to match different preferences (e.g. stddevweight=0 gives just the average, doesn’t subtract deviation). It’s probably best to run the script with different settings, decide what’s important for us and cross reference the results.
Github project can be found here (requires Python 2.7 + Beautiful Soup).