{"id":1558,"date":"2024-09-05T07:00:34","date_gmt":"2024-09-05T12:00:34","guid":{"rendered":"https:\/\/wollen.org\/blog\/?p=1558"},"modified":"2025-03-15T03:11:28","modified_gmt":"2025-03-15T08:11:28","slug":"how-common-is-roughing-the-passer","status":"publish","type":"post","link":"https:\/\/wollen.org\/blog\/2024\/09\/how-common-is-roughing-the-passer\/","title":{"rendered":"How common is Roughing the Passer?"},"content":{"rendered":"<p>The NFL&#8217;s enforcement of this rule has long been a source of frustration for fans. Minor incidental contact with a quarterback can suddenly trigger a 15-yard penalty and an automatic first down, which can easily change the course of a game.<\/p>\n<p>On the other hand, the quarterback position has become so important that an injury usually leads to an obvious drop in play quality. The league needs to protect their stars but fans still expect a full-contact sport.<\/p>\n<p>How much contact with the quarterback is okay? That standard has change over the years\u2014most notably in 2009, 2018, and 2022, which I&#8217;ll highlight below.<\/p>\n<p>Let&#8217;s take a look at how many Roughing the Passer calls are made each season and how it&#8217;s changed over time. For a few reasons, more passes are attempted today than 25 years ago. We can account for this by analyzing the <strong>rate<\/strong> of penalties. In other words, how many Roughing calls are made per pass attempt.<\/p>\n<hr \/>\n<h4>1. Prepare the data.<\/h4>\n<p>This script will use play-by-play data from <a href=\"https:\/\/pypi.org\/project\/nfl-data-py\/\" target=\"_blank\" rel=\"noopener\">nfl-data-py<\/a>, a fantastic Python library that makes life easier for the sports\/data nerds out there. It provides a wide range of NFL info, from scores to rosters to weather. Play-by-play data is available beginning with the 1999-2000 season so that&#8217;s where we&#8217;ll start.<\/p>\n<p><code>import_pbp_data()<\/code> returns a pandas Dataframe with everything we need. Its one required parameter, <code>years<\/code>, accepts a range of values, but 24 seasons is too much for my PC to handle. I&#8217;ll break the code into a <code>for<\/code> loop and process each season individually.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import nfl_data_py as nfl\r\n\r\nfor n in range(1999, 2024):\r\n    df = nfl.import_pbp_data(years=[n])<\/pre>\n<p>There are 372 columns in the Dataframe so I won&#8217;t try to display the whole thing here. Fortunately this script only needs two of them.<\/p>\n<p>Our approach will be to check (1) the number of passing plays and (2) the number of Roughing the Passer penalties. We can then divide those figures to calculate a &#8220;roughing rate&#8221;.<\/p>\n<div style=\"height: 1px;\"><\/div>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_12pt.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-1566\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_12pt.png\" alt=\"\" width=\"484\" height=\"45\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_12pt.png 484w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_12pt-300x28.png 300w\" sizes=\"auto, (max-width: 484px) 100vw, 484px\" \/><\/a><\/p>\n<div style=\"height: 1px;\"><\/div>\n<p>We need to look at the <em>play_type<\/em> column (pass, run, extra point, etc.) and <em>penalty_type<\/em> (false start, delay of game, etc.). For each season in the loop, append roughing rate to the <code>y_roughing_rate<\/code> list.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">x_season = range(1999, 2024)\r\ny_roughing_rate = []\r\n\r\nfor season in x_season:\r\n    df = nfl.import_pbp_data(years=[season])\r\n\r\n    passing_plays = df[df['play_type'] == \"pass\"].shape[0]\r\n\r\n    roughing_penalties = df[df['penalty_type'] == \"Roughing the Passer\"].shape[0]\r\n\r\n    y_roughing_rate.append(roughing_penalties \/ passing_plays * 100)<\/pre>\n<p>Now we have two lists and we can create a bar graph.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">x_season = [1999, 2000, 2001, ...]\r\ny_roughing_rate = [0.38428, 0.43396, 0.43397, ...]<\/pre>\n<hr \/>\n<h4>2. Plot the data.<\/h4>\n<p>This plot uses a custom NFL-themed Matplotlib style that I&#8217;ll link at the bottom of this post. I think it does a pretty good job of copying the leagues branding but not so good that I&#8217;ll get a cease-and-desist from Roger Goodell.<\/p>\n<p>Create an axes object and then call <code>bar()<\/code>. We&#8217;ll do a couple style things later so specify a layer, <code>zorder=2<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import matplotlib.pyplot as plt\r\n\r\nplt.style.use(\"wollen_nfl.mplstyle\")\r\nfig, ax = plt.subplots()\r\n\r\nax.bar(x_season, y_roughing_rate, width=0.6, zorder=2)<\/pre>\n<p>x-ticks are rotated 60 degrees so they can be more densely packed along the horizontal axis. y-tick labels need to be formatted with a % sign because we multiplied values by 100 in the code above.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">x_ticks = range(1999, 2024)\r\nax.set_xticks(x_ticks)\r\nplt.setp(ax.xaxis.get_majorticklabels(), rotation=60, ha=\"right\", rotation_mode=\"anchor\")\r\nax.set_xlim(1998.2, 2023.6)\r\n\r\ny_ticks = arange(0.0, 0.9, 0.1)\r\nax.set_yticks(y_ticks, labels=[f\"{n:.1f}%\" for n in y_ticks])\r\nax.set_ylim(0, y_ticks[-1] * 1.01)\r\n\r\nax.set_ylabel(\"% of Pass Plays\")\r\nax.set_title(\"NFL  \u2022  Roughing the Passer Penalties\")<\/pre>\n<p>I mentioned that we&#8217;ll highlight three rule changes&#8230;<\/p>\n<hr style=\"width: 50%;\" \/>\n<p><strong>(1)<\/strong> In his 2008 season opener, Tom Brady tore both his ACL and MCL when a defender lunged at his knees. It didn&#8217;t appear to be a dirty play but Brady missing the season was a significant loss for the league. Before the next season began, Roughing the Passer was amended to prohibit forcibly hitting a passer&#8217;s lower leg.<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/brady_injury_2008.gif\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-1569\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/brady_injury_2008.gif\" alt=\"\" width=\"350\" height=\"197\" \/><\/a><\/p>\n<p><strong>(2)<\/strong> In 2017, a Vikings defender tackled Aaron Rodgers and the impact of his bodyweight broke Rodgers&#8217; collarbone. Once again, after the season concluded, Roughing the Passer was updated to address the incident. The rule now prohibits defenders from landing on a quarterback with all or most of their weight.<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/rodgers_injury_2017.gif\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-1570\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/rodgers_injury_2017.gif\" alt=\"\" width=\"350\" height=\"197\" \/><\/a><\/p>\n<p><strong>(3)<\/strong> In the 2022 season, the league attempted to cut down on penalties and improve the competitive balance discussed above. The rule now permits <em>some<\/em> incidental contact to the head, neck, and lower leg areas.<\/p>\n<hr style=\"width: 50%;\" \/>\n<p>Let&#8217;s label the rule changes on our plot to give the viewer more context. Use <code>annotate()<\/code> to place text and an arrow at each point. I like to package annotations into a list of tuples and iterate through them.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">highlight_coords = [(2008, \"Tom Brady\\nknee\", \"center\"),\r\n                    (2017, \"Aaron Rodgers\\ncollarbone\", \"center\"),\r\n                    (2022, '\"Incidental\\ncontact\"\\nchange', \"left\")]\r\n\r\nfor year, text, align in highlight_coords:\r\n    ax.annotate(text, ha=align,\r\n                xy=(year, y_roughing_rate[year - 1999]), xytext=(year, y_roughing_rate[year - 1999] + 0.1),\r\n                arrowprops={\"arrowstyle\": \"wedge\", \"color\": \"#DFDFDF\"})<\/pre>\n<p>Credit the data source with <code>text()<\/code> in the upper-right corner.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">x_left, x_right = ax.get_xlim()\r\nx_span = x_right - x_left\r\nax.text(x_right - x_span * 0.005, y_ticks[-1] * 0.992, \"Data: nfl-data-py.\", ha=\"right\", va=\"top\")<\/pre>\n<p>I&#8217;d like to create a background gradient rather than using a solid color. The colors will be blue and slightly darker blue, so the effect will be subtle, but I think it will add some needed depth.<\/p>\n<p>Use the <a href=\"https:\/\/pypi.org\/project\/colour\/\" target=\"_blank\" rel=\"noopener\">colour<\/a> library to generate a list of colors. I&#8217;ve found the easiest way to draw a gradient across the <em>whole<\/em> figure is to use <code>imshow()<\/code> and set <code>clip_on=False<\/code>. The approach is to create a tall, narrow 2-D array and stretch its <code>extent<\/code> well beyond the window limits. Set <code>zorder<\/code> to 0 so it&#8217;s drawn on the bottom layer.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from colour import Color\r\n\r\ncolor_list = [c.rgb for c in Color(\"#0067B1\").range_to(Color(\"#013369\"), 100)]\r\ngradient_array = [[c] for c in color_list]\r\nplt.imshow(gradient_array, extent=(1990, 2030, -0.1, 1.2),\r\n           interpolation=\"bilinear\", aspect=\"auto\", clip_on=False, zorder=0)<\/pre>\n<p>As a final touch, include the NFL logo with low <code>alpha<\/code> to create a watermark effect. Setting <code>zorder<\/code> to 1 will layer it beneath the bars and above the background.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from matplotlib.offsetbox import OffsetImage, AnnotationBbox\r\n\r\nab = AnnotationBbox(OffsetImage(plt.imread(\"nfl_logo.png\"), zoom=0.15, alpha=0.03),\r\n                    (x_left, y_ticks[-1]), box_alignment=(0, 1), frameon=False, zorder=1)\r\nax.add_artist(ab)<\/pre>\n<p>Finally, save the figure with a bumped <code>dpi<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">plt.savefig(\"roughing_rate_nfl.png\", dpi=150)<\/pre>\n<hr \/>\n<h4>3. The output.<\/h4>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_nfl-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-2245 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_nfl-1.png\" alt=\"\" width=\"1800\" height=\"975\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_nfl-1.png 1800w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_nfl-1-300x163.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_nfl-1-1024x555.png 1024w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_nfl-1-768x416.png 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/09\/roughing_rate_nfl-1-1536x832.png 1536w\" sizes=\"auto, (max-width: 1800px) 100vw, 1800px\" \/><\/a><\/p>\n<p>You can see that Roughing the Passer calls have been less common over the past two seasons. The decision to allow slightly more incidental contact has successfully reduced the penalty rate.<\/p>\n<p>It&#8217;s worth remembering that when officials are directed to judge contact differently, players update their behavior as well. For example, calls became more frequent after the 2009 rule change but then leveled off around 2011, possibly because defenders had adapted to the change. A new equilibrium was reached and (hopefully) the rule had its desired effect. The idea isn&#8217;t solely to punish; it&#8217;s to change how the game is played.<\/p>\n<hr \/>\n<p><a href=\"https:\/\/wollen.org\/misc\/nfl_roughing_2024.zip\"><strong>Download the data.<\/strong><\/a><\/p>\n<p><strong>Full code:<\/strong><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import nfl_data_py as nfl\r\nimport matplotlib.pyplot as plt\r\nfrom matplotlib.offsetbox import OffsetImage, AnnotationBbox\r\nfrom numpy import arange\r\n\r\n\r\nx_season = range(1999, 2024)\r\ny_roughing_rate = []\r\n\r\nfor season in x_season:\r\n    df = nfl.import_pbp_data(years=[season])\r\n\r\n    passing_plays = df[df['play_type'] == \"pass\"].shape[0]\r\n\r\n    roughing_penalties = df[df['penalty_type'] == \"Roughing the Passer\"].shape[0]\r\n\r\n    y_roughing_rate.append(roughing_penalties \/ passing_plays * 100)\r\n\r\nplt.style.use(\"wollen_nfl.mplstyle\")\r\nfig, ax = plt.subplots()\r\n\r\nax.bar(x_season, y_roughing_rate, width=0.6, zorder=2)\r\n\r\nx_ticks = range(1999, 2024)\r\nax.set_xticks(x_ticks)\r\nplt.setp(ax.xaxis.get_majorticklabels(), rotation=60, ha=\"right\", rotation_mode=\"anchor\")\r\nax.set_xlim(1998.2, 2023.6)\r\n\r\ny_ticks = arange(0.0, 0.9, 0.1)\r\nax.set_yticks(y_ticks, labels=[f\"{n:.1f}%\" for n in y_ticks])\r\nax.set_ylim(0, y_ticks[-1] * 1.01)\r\n\r\nax.set_ylabel(\"% of Pass Plays\")\r\nax.set_title(\"NFL  \u2022  Roughing the Passer Penalties\")\r\n\r\nhighlight_coords = [(2008, \"Tom Brady\\nknee\", \"center\"),\r\n                    (2017, \"Aaron Rodgers\\ncollarbone\", \"center\"),\r\n                    (2022, '\"Incidental\\ncontact\"\\nchange', \"left\")]\r\n\r\nfor year, text, align in highlight_coords:\r\n    ax.annotate(text, ha=align,\r\n                xy=(year, y_roughing_rate[year - 1999]), xytext=(year, y_roughing_rate[year - 1999] + 0.1),\r\n                arrowprops={\"arrowstyle\": \"wedge\", \"color\": \"#DFDFDF\"})\r\n\r\nx_left, x_right = ax.get_xlim()\r\nx_span = x_right - x_left\r\nax.text(x_right - x_span * 0.005, y_ticks[-1] * 0.992, \"Data: nfl-data-py.\", ha=\"right\", va=\"top\")\r\n\r\ncolor_list = [c.rgb for c in Color(\"#0067B1\").range_to(Color(\"#013369\"), 100)]\r\ngradient_array = [[c] for c in color_list]\r\nplt.imshow(gradient_array, extent=(1990, 2030, -0.1, 1.2),\r\n           interpolation=\"bilinear\", aspect=\"auto\", clip_on=False, zorder=0)\r\n\r\nab = AnnotationBbox(OffsetImage(plt.imread(\"nfl_logo.png\"), zoom=0.15, alpha=0.03),\r\n                    (x_left, y_ticks[-1]), box_alignment=(0, 1), frameon=False, zorder=1)\r\nax.add_artist(ab)\r\n\r\nplt.savefig(\"roughing_rate_nfl.png\", dpi=150)<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The NFL&#8217;s enforcement of this rule has long been a source of frustration for fans. Minor incidental contact with a quarterback can suddenly trigger a 15-yard penalty and an automatic first down, which can easily change the course of a<\/p>\n","protected":false},"author":1,"featured_media":1618,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[59],"tags":[320,327,317,136,318,331,332,319,333,328,329,22,122,120,330,173,24,119,316,325,30,315,46,25,324,323,322,326,60,321],"class_list":["post-1558","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-sports","tag-aaron-rodgers","tag-analysis","tag-annotation","tag-annotationbbox","tag-arrow","tag-background","tag-background-gradient","tag-bar","tag-clip_on","tag-color","tag-colour","tag-data","tag-dataset","tag-football","tag-gradient","tag-graph","tag-matplotlib","tag-nfl","tag-nfl-data-py","tag-official","tag-pandas","tag-penalties","tag-plot","tag-python","tag-referee","tag-roughing","tag-roughing-the-passer","tag-rules","tag-sports","tag-tom-brady"],"_links":{"self":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/1558","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=1558"}],"version-history":[{"count":38,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/1558\/revisions"}],"predecessor-version":[{"id":2342,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/1558\/revisions\/2342"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media\/1618"}],"wp:attachment":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media?parent=1558"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/categories?post=1558"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/tags?post=1558"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}