Continue learning how to use Python to build a 2021 draft model and use ADP data to maximize value in your drafts.
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 continue to work on our 2021 VOR draft model. Last time we simply created our VOR rankings. This time, we are going to be comparing those rankings to ADP to find players who are currently being underdrafted or overdrated.
For example, if a player is being drafted at pick #100 and our VOR model has them ranked player #120, they are being overdrafted. We should avoid them, until we can grab at pick #120 or greater. Truth be told, you can do this with any ranking model - but the exercise is good practice in joining DataFrames together (and actually is pretty useful for your drafts). In previous editions of our draft model, I've gone in to more detail in how looking at ADP can help give you an edge in your drafts.
As always, we are going to start out this tutorial by importing our standard libraries.
If the below block of code confuses you, check out the last post in this series! Here, we're simply regenerating the VOR model we came up with last time in one, single block of code.
Player | Team | POS | RUSH_ATT | RUSH_YD | RUSH_TD | REC | REC_YD | REC_TD | FL | PASS_ATT | CMP | PASS_YD | PASS_TD | INTS | STANDARD | HALF_PPR | PPR | REPLACEMENT_VALUE | VOR | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Christian McCaffrey | CAR | RB | 297.2 | 1301.9 | 12.0 | 93.8 | 760.1 | 4.3 | 2.5 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 299.00 | 345.90 | 392.80 | 139.66 | 253.14 |
1 | Dalvin Cook | MIN | RB | 316.6 | 1487.7 | 12.9 | 54.8 | 472.9 | 2.0 | 2.9 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 279.66 | 307.06 | 334.46 | 139.66 | 194.80 |
2 | Alvin Kamara | NO | RB | 206.9 | 941.6 | 9.7 | 81.0 | 689.3 | 3.5 | 1.9 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 238.49 | 278.99 | 319.49 | 139.66 | 179.83 |
3 | Derrick Henry | TEN | RB | 343.1 | 1703.7 | 14.2 | 23.8 | 178.6 | 1.1 | 2.7 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 274.63 | 286.53 | 298.43 | 139.66 | 158.77 |
4 | Travis Kelce | KC | TE | 0.0 | 0.0 | 0.0 | 112.4 | 1399.3 | 10.2 | 0.8 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 199.53 | 255.73 | 311.93 | 154.76 | 157.17 |
Now, we need to doo two things differently than we did last time. We need to join the ADP dataframe with the VOR dataframe generated above, and also calculate the difference between our VOR rank and ADP. We can do this all in a single line of code using merge and assign. The assign method isn't a method I've used frequently in my content. It's simply another way of assigning a column to a DataFrame, and can be useful in certain situations.
Player | Team | POS | VOR | VOR_RANK | ADP_RANK | OVER_UNDER_DRAFTED | |
---|---|---|---|---|---|---|---|
0 | Christian McCaffrey | CAR | RB | 253.140 | 1.0 | 1.0 | 0.0 |
1 | Dalvin Cook | MIN | RB | 194.800 | 2.0 | 2.0 | 0.0 |
2 | Alvin Kamara | NO | RB | 179.830 | 3.0 | 3.0 | 0.0 |
3 | Derrick Henry | TEN | RB | 158.770 | 4.0 | 4.0 | 0.0 |
4 | Travis Kelce | KC | TE | 157.170 | 5.0 | 8.0 | 3.0 |
5 | Davante Adams | GB | WR | 150.650 | 6.0 | 6.0 | 0.0 |
6 | Saquon Barkley | NYG | RB | 142.990 | 7.0 | 7.0 | 0.0 |
7 | Austin Ekeler | LAC | RB | 142.000 | 8.0 | 13.0 | 5.0 |
8 | Ezekiel Elliott | DAL | RB | 138.940 | 9.0 | 5.0 | -4.0 |
9 | Aaron Jones | GB | RB | 137.520 | 10.0 | 10.0 | 0.0 |
10 | Tyreek Hill | KC | WR | 135.960 | 11.0 | 12.0 | 1.0 |
11 | Jonathan Taylor | IND | RB | 132.200 | 12.0 | 11.0 | -1.0 |
12 | Joe Mixon | CIN | RB | 125.340 | 13.0 | 21.0 | 8.0 |
13 | Stefon Diggs | BUF | WR | 123.380 | 14.0 | 14.0 | 0.0 |
14 | Najee Harris | PIT | RB | 119.730 | 15.0 | 15.0 | 0.0 |
15 | DeAndre Hopkins | ARI | WR | 119.240 | 16.0 | 16.0 | 0.0 |
16 | Nick Chubb | CLE | RB | 116.980 | 17.0 | 9.0 | -8.0 |
17 | Antonio Gibson | WAS | RB | 112.120 | 18.0 | 17.0 | -1.0 |
18 | Clyde Edwards-Helaire | KC | RB | 103.160 | 19.0 | 24.0 | 5.0 |
19 | Calvin Ridley | ATL | WR | 102.100 | 20.0 | 20.0 | 0.0 |
20 | D'Andre Swift | DET | RB | 101.380 | 21.0 | 34.0 | 13.0 |
21 | Darren Waller | LV | TE | 100.790 | 22.0 | 22.0 | 0.0 |
22 | George Kittle | SF | TE | 100.380 | 23.0 | 26.0 | 3.0 |
23 | Chris Carson | SEA | RB | 96.160 | 24.0 | 33.0 | 9.0 |
24 | David Montgomery | CHI | RB | 90.440 | 25.0 | 30.0 | 5.0 |
25 | Miles Sanders | PHI | RB | 87.970 | 26.0 | 37.0 | 11.0 |
26 | Keenan Allen | LAC | WR | 86.550 | 27.0 | 27.0 | 0.0 |
27 | Justin Jefferson | MIN | WR | 85.310 | 28.0 | 23.0 | -5.0 |
28 | Josh Jacobs | LV | RB | 81.200 | 29.0 | 36.0 | 7.0 |
29 | D.K. Metcalf | SEA | WR | 80.130 | 30.0 | 19.0 | -11.0 |
30 | A.J. Brown | TEN | WR | 75.770 | 31.0 | 25.0 | -6.0 |
31 | Josh Allen | BUF | QB | 75.224 | 32.0 | 29.0 | -3.0 |
32 | Mike Davis | ATL | RB | 75.170 | 33.0 | 54.0 | 21.0 |
33 | Darrell Henderson | LAR | RB | 73.460 | 34.0 | 59.0 | 25.0 |
34 | Patrick Mahomes | KC | QB | 72.166 | 35.0 | 18.0 | -17.0 |
35 | James Robinson | JAC | RB | 70.260 | 36.0 | 42.0 | 6.0 |
36 | Chase Edmonds | ARI | RB | 70.080 | 37.0 | 63.0 | 26.0 |
37 | Allen Robinson | CHI | WR | 68.690 | 38.0 | 31.0 | -7.0 |
38 | Robert Woods | LAR | WR | 62.820 | 39.0 | 38.0 | -1.0 |
39 | Terry McLaurin | WAS | WR | 61.300 | 40.0 | 28.0 | -12.0 |
40 | Amari Cooper | DAL | WR | 59.650 | 41.0 | 40.0 | -1.0 |
41 | Tyler Lockett | SEA | WR | 55.650 | 42.0 | 48.0 | 6.0 |
42 | Kyler Murray | ARI | QB | 53.538 | 43.0 | 39.0 | -4.0 |
43 | Myles Gaskin | MIA | RB | 52.530 | 44.0 | 49.0 | 5.0 |
44 | Cooper Kupp | LAR | WR | 50.680 | 45.0 | 45.0 | 0.0 |
45 | Mike Evans | TB | WR | 49.180 | 46.0 | 35.0 | -11.0 |
46 | Mark Andrews | BAL | TE | 48.640 | 47.0 | 50.0 | 3.0 |
47 | CeeDee Lamb | DAL | WR | 47.350 | 48.0 | 32.0 | -16.0 |
48 | Kareem Hunt | CLE | RB | 45.670 | 49.0 | 53.0 | 4.0 |
49 | Adam Thielen | MIN | WR | 45.350 | 50.0 | 47.0 | -3.0 |
50 | Lamar Jackson | BAL | QB | 43.020 | 51.0 | 44.0 | -7.0 |
51 | D.J. Moore | CAR | WR | 41.020 | 52.0 | 51.0 | -1.0 |
52 | Chris Godwin | TB | WR | 39.170 | 53.0 | 41.0 | -12.0 |
53 | Diontae Johnson | PIT | WR | 37.030 | 54.0 | 52.0 | -2.0 |
54 | Julio Jones | TEN | WR | 36.650 | 55.0 | 43.0 | -12.0 |
55 | Dak Prescott | DAL | QB | 36.486 | 56.0 | 56.0 | 0.0 |
56 | Kyle Pitts | ATL | TE | 34.660 | 57.0 | 46.0 | -11.0 |
57 | T.J. Hockenson | DET | TE | 33.730 | 58.0 | 58.0 | 0.0 |
58 | Tee Higgins | CIN | WR | 29.760 | 59.0 | 64.0 | 5.0 |
59 | Russell Wilson | SEA | QB | 28.106 | 60.0 | 55.0 | -5.0 |
60 | Aaron Rodgers | GB | QB | 27.790 | 61.0 | 57.0 | -4.0 |
61 | Raheem Mostert | SF | RB | 27.410 | 62.0 | 73.0 | 11.0 |
62 | Javonte Williams | DEN | RB | 25.650 | 63.0 | 61.0 | -2.0 |
63 | Justin Herbert | LAC | QB | 24.618 | 64.0 | 62.0 | -2.0 |
64 | JuJu Smith-Schuster | PIT | WR | 24.580 | 65.0 | 68.0 | 3.0 |
65 | Courtland Sutton | DEN | WR | 23.500 | 66.0 | 77.0 | 11.0 |
66 | Kenny Golladay | NYG | WR | 21.770 | 67.0 | 69.0 | 2.0 |
67 | Odell Beckham | CLE | WR | 21.690 | 68.0 | 65.0 | -3.0 |
68 | Brandon Aiyuk | SF | WR | 19.130 | 69.0 | 60.0 | -9.0 |
69 | Ja'Marr Chase | CIN | WR | 18.420 | 70.0 | 66.0 | -4.0 |
70 | Tom Brady | TB | QB | 17.972 | 71.0 | 72.0 | 1.0 |
71 | Brandin Cooks | HOU | WR | 17.950 | 72.0 | 89.0 | 17.0 |
72 | Robby Anderson | CAR | WR | 17.660 | 73.0 | 76.0 | 3.0 |
73 | Tyler Boyd | CIN | WR | 14.810 | 74.0 | 86.0 | 12.0 |
74 | Damien Harris | NE | RB | 14.520 | 75.0 | 74.0 | -1.0 |
75 | Leonard Fournette | TB | RB | 14.480 | 76.0 | 81.0 | 5.0 |
76 | Chase Claypool | PIT | WR | 12.530 | 77.0 | 67.0 | -10.0 |
77 | D.J. Chark | JAC | WR | 12.230 | 78.0 | 95.0 | 17.0 |
78 | Deebo Samuel | SF | WR | 7.750 | 79.0 | 82.0 | 3.0 |
79 | Logan Thomas | WAS | TE | 7.540 | 80.0 | 80.0 | 0.0 |
80 | Michael Carter | NYJ | RB | 6.120 | 81.0 | 84.0 | 3.0 |
81 | Kenyan Drake | LV | RB | 6.000 | 82.0 | 102.0 | 20.0 |
82 | Jerry Jeudy | DEN | WR | 5.790 | 83.0 | 71.0 | -12.0 |
83 | David Johnson | HOU | RB | 5.490 | 84.0 | 114.0 | 30.0 |
84 | Zack Moss | BUF | RB | 5.360 | 85.0 | 96.0 | 11.0 |
85 | Tyler Higbee | LAR | TE | 5.310 | 86.0 | 110.0 | 24.0 |
86 | Melvin Gordon | DEN | RB | 4.550 | 87.0 | 78.0 | -9.0 |
87 | J.D. McKissic | WAS | RB | 4.390 | 88.0 | 138.0 | 50.0 |
88 | Jamaal Williams | DET | RB | 3.080 | 89.0 | 116.0 | 27.0 |
89 | Noah Fant | DEN | TE | 2.260 | 90.0 | 79.0 | -11.0 |
90 | Dallas Goedert | PHI | TE | 1.640 | 91.0 | 87.0 | -4.0 |
91 | Jarvis Landry | CLE | WR | 0.000 | 93.5 | 100.0 | 6.5 |
92 | James Conner | ARI | RB | 0.000 | 93.5 | 99.0 | 5.5 |
93 | Robert Tonyan | GB | TE | 0.000 | 93.5 | 90.0 | -3.5 |
94 | Jalen Hurts | PHI | QB | 0.000 | 93.5 | 97.0 | 3.5 |
95 | Laviska Shenault | JAC | WR | -0.480 | 96.0 | 93.0 | -3.0 |
96 | Michael Gallup | DAL | WR | -0.680 | 97.0 | 115.0 | 18.0 |
97 | Ryan Tannehill | TEN | QB | -0.942 | 98.0 | 92.0 | -6.0 |
98 | James White | NE | RB | -3.220 | 99.0 | 160.0 | 61.0 |
99 | Trey Sermon | SF | RB | -3.600 | 100.0 | 85.0 | -15.0 |
How we use this information is simple. The more negative a number, the less we want to focus on a certain player. For example, Ezekiel Elliot ranks #9 on our VOR ranking list, but is currently being drafted, on average, at the #5 pick. If we are strictly following our model, we don't want to draft Zeke unless he drops to the #9 pick or farther. However, if Zeke does fall beyond #9, drafting him comes in to play again. In other words, we want to keep these values in mind, but we also want to consider where WE are currently drafting. Average draft position values are just that - average draft positions. Our league could play out differently, even though ADP gives us some indication of what is "most likely" to occur.
The likelihood that a player being drafted above value comes in to value is proportional to the spread between VOR rank and ADP rank. The more negative the difference between these two numbers, the less likely a player is to come in to a draftable position at some point during the draft.
The more postive the difference between these two numbers, the more likely it is a player will remain draftable for the majority or entirety of the draft. When targeting a player with a high positive VOR and ADP rank spread, our goal shouldn't be to snatch them up as quickly as possible, though. Our goal should be to use ADP data to pick them as close to ADP (or even beyond ADP) as possible, maximizing the value we grab for the player. For example, Deandre Swift has a positive spread of +13 and is currenty being drafted, on average, at pick #34. Let's say you are pick #4 in a snake draft, 12-man league. This league format would render you pick #4, #21, #28, and #45 for your first 4 picks. At your second pick (#21), Swift is technically draftable because his VOR rank is at least equal to your current draft position. However, using ADP data, we can see that Swift is being drafted around #34, and your next pick is #28. Because there is such a large cushion (6 picks, which represents roughly 20% of the draft that has already taken place) between Swift's ADP and your next draft pick, you can probably take the risk of passing on Swift now and try to grab him at an even better value in the next round. Pick #28, your third pick, will probably be the last chance you'll have to draft Swift, though. By the time of your fourth pick, #45, Swift will most likely already be drafted.
Instead of drafting Swift with your #21 pick, it would probably be smarter to draft Waller, who is unlikely to be available by the next round (even though you would technically be over-reaching by 1 pick, which doesn't really matter).
Using ADP data is the final puzzle piece in putting together a comprehensive draft plan. Ranking models tell you who to prioritize over who, which is only part of the equation. Using ADP allows us to execute on our ranking model and gives us the best chance of finding "steals" or sleepers that will maximize our pick value at each draft turn.
That's it for this part of the intermediate series - thank you for reading!