{"id":1011,"date":"2024-03-05T17:00:48","date_gmt":"2024-03-05T23:00:48","guid":{"rendered":"https:\/\/wollen.org\/blog\/?p=1011"},"modified":"2024-08-17T07:12:49","modified_gmt":"2024-08-17T12:12:49","slug":"xavier-worthy-40-yard-dash","status":"publish","type":"post","link":"https:\/\/wollen.org\/blog\/2024\/03\/xavier-worthy-40-yard-dash\/","title":{"rendered":"Xavier Worthy&#8217;s record-breaking 4.21 40-yard dash"},"content":{"rendered":"<p>If you&#8217;re an NFL fan you probably heard the buzz from this weekend&#8217;s <a href=\"https:\/\/www.nfl.com\/combine\/\" target=\"_blank\" rel=\"noopener\">Scouting Combine<\/a>: former Texas wide receiver <a href=\"https:\/\/en.wikipedia.org\/wiki\/Xavier_Worthy\" target=\"_blank\" rel=\"noopener\">Xavier Worthy<\/a> set a new event record in the 40-yard dash (4.21 seconds). Whether that means he&#8217;s destined for the Hall of Fame\u2014or absolutely nothing\u2014is up for debate. Fans of Worthy&#8217;s first NFL team might want to overlook the Combine&#8217;s spotty record of predicting success in the league.<\/p>\n<p>I&#8217;m not going to do a full analysis of Combine measurables and how they correlate with NFL performance (although that would be fun to read!). But I thought it would be fun to plot Worthy&#8217;s athleticism, measured in terms of 40-yard dash time and vertical leap, in the context of all the Combine&#8217;s previous wide receivers.<\/p>\n<p>Or at least the wide receivers who have come through since 2000, because that&#8217;s as far back as <a href=\"https:\/\/www.pro-football-reference.com\/draft\/2000-combine.htm\" target=\"_blank\" rel=\"noopener\">Pro Football Reference<\/a> goes. I&#8217;ll link the dataset at the bottom of this post.<\/p>\n<hr \/>\n<h4>1. Prepare the data.<\/h4>\n<p>Start by importing pandas, matplotlib, and <code>numpy.arange()<\/code>, which we&#8217;ll use for x-ticks.<\/p>\n<p>The data requires minimal processing. We can simply:<\/p>\n<ol>\n<li>Read the CSV file into a pandas DataFrame.<\/li>\n<li>Create a <em>view<\/em> containing only wide receiver rows.<\/li>\n<li>Drop rows where either 40-yard dash or vertical leap data is missing.<\/li>\n<\/ol>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\nimport matplotlib.pyplot as plt\r\nfrom numpy import arange\r\n\r\ndf = pd.read_csv(\"nfl_combine_2000-2023.csv\")\r\ndf = df[df[\"position\"] == \"WR\"]\r\ndf = df.dropna(subset=[\"forty\", \"vertical\"])<\/pre>\n<p>This takes us from 7,999 rows down to 896, which we can turn into a scatter plot.<\/p>\n<hr \/>\n<h4>2. Plot the data.<\/h4>\n<p>I&#8217;m using a custom <em>mplstyle<\/em> meant to mimic <a href=\"https:\/\/ggplot2.tidyverse.org\/\" target=\"_blank\" rel=\"noopener\">R&#8217;s ggplot2<\/a>. Matplotlib provides a similar style (and many others) when you install the library. It&#8217;s easy to tweak those files or to create your own from scratch. It&#8217;s often an easier way to achieve your desired appearance than to write extra lines of Python.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">plt.style.use(\"wollen_ggplot.mplstyle\")\r\nfig, ax = plt.subplots(figsize=(9, 9))\r\n<\/pre>\n<p>The dataset contains Combine results from 2000 through 2023. We&#8217;ll separately add Xavier Worthy&#8217;s 2024 measurements, assign his dot a unique appearance, and identify it with a legend in the upper-left corner.<\/p>\n<p>When plotting data like this with many overlapping points, it helps to lower the scatter&#8217;s <code>alpha<\/code>, i.e. increase transparency. A darker color indicates that dots are stacked on top of each other.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">ax.scatter(df[\"forty\"], df[\"vertical\"], alpha=0.5)\r\nax.scatter([4.21], [41], marker=\"D\", s=50, label=\"Xavier Worthy (2024)\")<\/pre>\n<p>I like to define x-ticks and y-ticks so that the data is entirely within the tick range. In other words the topmost data point doesn&#8217;t poke above the top y-axis line.<\/p>\n<p>And then I define the axis window in terms of those limits. Usually a 1% margin looks fine. Sometimes tick labels take up more space and it helps to have a larger margin, or to rotate the labels, but this plot doesn&#8217;t cause any issues.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">x_ticks = arange(4.2, 5.0, 0.1)\r\nax.set_xticks(x_ticks)\r\nx_tick_span = x_ticks[-1] - x_ticks[0]\r\nx_left, x_right = x_ticks[0] - x_tick_span * 0.015, x_ticks[-1] + x_tick_span * 0.015\r\nax.set_xlim(x_left, x_right)\r\n\r\ny_ticks = range(26, 50, 2)\r\nax.set_yticks(y_ticks)\r\ny_tick_span = y_ticks[-1] - y_ticks[0]\r\ny_bottom, y_top = y_ticks[0] - y_tick_span * 0.015, y_ticks[-1] + y_tick_span * 0.015\r\nax.set_ylim(y_bottom, y_top)\r\n\r\nax.set_xlabel(\"40-Yard Dash (s)\")\r\nax.set_ylabel(\"Vertical  (inches)\")\r\nax.set_title(\"NFL Combine  |  Wide Receivers  |  2000\u20132023\")<\/pre>\n<p>We should add a citation to Pro Football Reference, again defining its position in terms of the data.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">x_range = x_right - x_left\r\ny_range = y_top - y_bottom\r\nax.text(x_right - x_range * 0.005, y_top - y_range * 0.007,\r\n        \"Data: www.pro-football-reference.com.\",\r\n        size=9, ha=\"right\", va=\"top\")<\/pre>\n<p>Finally, turn on a legend to annotate Worthy&#8217;s performance and save the figure to a file. I usually bump up the default <code>dpi<\/code> to give a cleaner presentation.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">ax.legend(loc=\"upper left\")\r\n\r\nplt.savefig(\"WR_40_vertical.png\", dpi=200)<\/pre>\n<hr \/>\n<h4>3. The output.<\/h4>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/03\/WR_40_vertical-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-1746 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/03\/WR_40_vertical-1.png\" alt=\"\" width=\"1800\" height=\"1800\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/03\/WR_40_vertical-1.png 1800w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/03\/WR_40_vertical-1-300x300.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/03\/WR_40_vertical-1-1024x1024.png 1024w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/03\/WR_40_vertical-1-150x150.png 150w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/03\/WR_40_vertical-1-768x768.png 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/03\/WR_40_vertical-1-1536x1536.png 1536w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/03\/WR_40_vertical-1-800x800.png 800w\" sizes=\"auto, (max-width: 1800px) 100vw, 1800px\" \/><\/a>You can see Xavier Worthy and his record-breaking 40-yard dash as the farthest-left dot. John Ross, to Worthy&#8217;s right, held the record previously but with a 4-inch lower vertical. Worthy has deservedly gotten attention for his speed but we&#8217;ve shown that he&#8217;s also one of the best overall athletes to come through the draft.<\/p>\n<hr \/>\n<p><a href=\"https:\/\/wollen.org\/misc\/nfl_combine_3-6-2024.zip\">Download the data.<\/a><\/p>\n<p><strong>Full code:<\/strong><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\nimport matplotlib.pyplot as plt\r\nfrom numpy import arange\r\n\r\n\r\ndf = pd.read_csv(\"nfl_combine_2000-2023.csv\")\r\ndf = df[df[\"position\"] == \"WR\"]\r\ndf = df.dropna(subset=[\"forty\", \"vertical\"])\r\n\r\nplt.style.use(\"wollen_ggplot.mplstyle\")\r\nfig, ax = plt.subplots(figsize=(9, 9))\r\n\r\nax.scatter(df[\"forty\"], df[\"vertical\"], alpha=0.5)\r\nax.scatter([4.21], [41], marker=\"D\", s=50, label=\"Xavier Worthy (2024)\")\r\n\r\nx_ticks = arange(4.2, 5.0, 0.1)\r\nax.set_xticks(x_ticks)\r\nx_tick_span = x_ticks[-1] - x_ticks[0]\r\nx_left, x_right = x_ticks[0] - x_tick_span * 0.015, x_ticks[-1] + x_tick_span * 0.015\r\nax.set_xlim(x_left, x_right)\r\n\r\ny_ticks = range(26, 50, 2)\r\nax.set_yticks(y_ticks)\r\ny_tick_span = y_ticks[-1] - y_ticks[0]\r\ny_bottom, y_top = y_ticks[0] - y_tick_span * 0.015, y_ticks[-1] + y_tick_span * 0.015\r\nax.set_ylim(y_bottom, y_top)\r\n\r\nax.set_xlabel(\"40-Yard Dash (s)\")\r\nax.set_ylabel(\"Vertical  (inches)\")\r\nax.set_title(\"NFL Combine  |  Wide Receivers  |  2000\u20132023\")\r\n\r\nx_range = x_right - x_left\r\ny_range = y_top - y_bottom\r\nax.text(x_right - x_range * 0.005, y_top - y_range * 0.007,\r\n        \"Data: www.pro-football-reference.com.\",\r\n        size=9, ha=\"right\", va=\"top\")\r\n\r\nax.legend(loc=\"upper left\")\r\n\r\nplt.savefig(\"WR_40_vertical.png\", dpi=200)<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you&#8217;re an NFL fan you probably heard the buzz from this weekend&#8217;s Scouting Combine: former Texas wide receiver Xavier Worthy set a new event record in the 40-yard dash (4.21 seconds). Whether that means he&#8217;s destined for the Hall<\/p>\n","protected":false},"author":1,"featured_media":1036,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[59],"tags":[200,199,39,135,22,122,120,198,24,126,119,75,30,25,201,202,60,197,196,195,194],"class_list":["post-1011","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-sports","tag-40-time","tag-40-yard-dash","tag-code","tag-csv","tag-data","tag-dataset","tag-football","tag-jump","tag-matplotlib","tag-mplstyle","tag-nfl","tag-numpy","tag-pandas","tag-python","tag-scatter","tag-scatter-plot","tag-sports","tag-vertical","tag-worthy","tag-xavier","tag-xavier-worthy"],"_links":{"self":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/1011","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/comments?post=1011"}],"version-history":[{"count":31,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/1011\/revisions"}],"predecessor-version":[{"id":1747,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/1011\/revisions\/1747"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media\/1036"}],"wp:attachment":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media?parent=1011"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/categories?post=1011"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/tags?post=1011"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}