{"id":837,"date":"2021-11-19T07:00:40","date_gmt":"2021-11-19T13:00:40","guid":{"rendered":"https:\/\/wollen.org\/blog\/?p=837"},"modified":"2024-05-15T03:36:26","modified_gmt":"2024-05-15T08:36:26","slug":"how-does-matt-amodio","status":"publish","type":"post","link":"https:\/\/wollen.org\/blog\/2021\/11\/how-does-matt-amodio\/","title":{"rendered":"How does Matt Amodio&#8217;s Jeopardy streak compare?"},"content":{"rendered":"<p>If you&#8217;re reading this blog there&#8217;s a good chance you heard about Matt Amodio&#8217;s incredible run on <em>Jeopardy<\/em>. After winning 38 consecutive games he walked away with over $1.5 million dollars. I thought I&#8217;d take a look at how his streak fits into <em>Jeopardy<\/em> history.<\/p>\n<p>I&#8217;d like to visualize both <strong>total winnings<\/strong> and <strong>consecutive games won<\/strong>. I&#8217;ll use these variables to plot <strong>average daily winnings<\/strong>, which will check both boxes and hopefully provide some new information you haven&#8217;t seen a dozen times before.<\/p>\n<p>Conveniently, the show maintains a <a href=\"https:\/\/www.jeopardy.com\/contestant-zone\/hall-of-fame\" target=\"_blank\" rel=\"noopener\">Hall of Fame<\/a> with most of the data we need. I put the data into a CSV file you can download at the bottom of this post.<\/p>\n<hr \/>\n<h4>1. Prepare the data.<\/h4>\n<p>Start by reading the dataset into a pandas Dataframe.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">df = pd.read_csv(\"jeopardy_top_winners.csv\")<\/pre>\n<p>The show uses <strong>ALL CAPS<\/strong> in their text:<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_clue_ex-1.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-860\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_clue_ex-1-300x169.jpg\" alt=\"\" width=\"600\" height=\"338\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_clue_ex-1-300x169.jpg 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_clue_ex-1-768x432.jpg 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_clue_ex-1.jpg 840w\" sizes=\"auto, (max-width: 600px) 100vw, 600px\" \/><\/a><\/p>\n<p>So we&#8217;ll follow suit. Convert the name column with <code>upper()<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">df[\"name\"] = df[\"name\"].str.upper()<\/pre>\n<p>Now we can generate the data we intend to plot: average daily winnings. It&#8217;s as simple as dividing total winnings by number of games won. Create a new column to hold this information.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">df.loc[:, \"average\"] = df[\"total_won\"] \/ df[\"games_won\"]<\/pre>\n<p>Before jumping into Matplotlib I want to double-check the Dataframe.<\/p>\n<p>Although the primary objective is to plot average daily winnings, I&#8217;d like to order contestants by their number of games won. This should convey a second dimension of the data while also highlighting Amodio&#8217;s longevity.<\/p>\n<p>Notice that I pass a list into the <code>sort_values<\/code> method. Some contestants have won an equal number of games so I&#8217;ll use average winnings as the &#8220;tiebreaker.&#8221; You could include more tiebreakers if you wanted, but that won&#8217;t be necessary with this data.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">df = df.sort_values([\"games_won\", \"average\"]).reset_index(drop=True)\r\n\r\nprint(df)<\/pre>\n<p>The output:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">                 name  games_won  total_won       average\r\n0     JONATHAN FISHER         11     246100  22372.727273\r\n1          ARTHUR CHU         11     297200  27018.181818\r\n2         SETH WILSON         12     265002  22083.500000\r\n3       AUSTIN ROGERS         12     411000  34250.000000\r\n4        MATT JACKSON         13     411612  31662.461538\r\n5        DAVID MADDEN         19     430400  22652.631579\r\n6   JASON ZUFFRANIERI         19     532496  28026.105263\r\n7       JULIA COLLINS         20     428100  21405.000000\r\n8     JAMES HOLZHAUER         32    2462216  76944.250000\r\n9         MATT AMODIO         38    1518601  39963.184211\r\n10       KEN JENNINGS         74    2520700  34063.513514<\/pre>\n<p>The sorted values appear upside down right now, but notice the longest win streak (Ken Jennings) corresponds to the highest index (10). This will work because we&#8217;ll plot the data on a horizontal bar chart. It would be awkward and unnecessary to read the name text sideways. Let&#8217;s make it easy and turn the bars sideways instead.<\/p>\n<p>I&#8217;ve created a <em>Jeopardy<\/em>-themed Matplotlib style for this plot. The colors and text are designed to mimic the style of the show. For reference:<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_examples.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-857 size-large\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_examples-1024x327.jpg\" alt=\"\" width=\"640\" height=\"204\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_examples-1024x327.jpg 1024w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_examples-300x96.jpg 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_examples-768x245.jpg 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_examples-1536x491.jpg 1536w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_examples-2048x655.jpg 2048w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><\/p>\n<p>I also found a <a href=\"https:\/\/www.download-free-fonts.com\/details\/76190\/itc-korinna-bold\" target=\"_blank\" rel=\"noopener\">font<\/a> that copies clue text fairly well. We can use Matplotlib&#8217;s <em>Path Effects<\/em> to create a drop shadow and get even closer.<\/p>\n<hr \/>\n<h4>2. Plot the data.<\/h4>\n<p>Begin in the usual way by referencing an <code>mplstyle<\/code> and retrieving <code>fig<\/code> and <code>ax<\/code> objects.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">plt.style.use(\"jeopardy.mplstyle\")\r\n\r\nfig, ax = plt.subplots()<\/pre>\n<p>Horizontal bar charts are a little tricky\u2014at least for me\u2014because it&#8217;s easy to mix up the independent and dependent variables. I can never remember what Matplotlib considers <code>x<\/code> and <code>y<\/code> or which argument should be passed first.<\/p>\n<p>Confessions out of the way, the independent (vertical) iterable is passed first. This is <code>df.index<\/code> (0-10, seen above). Remember we&#8217;re plotting names so we&#8217;ll have to replace integer tick labels with text in a moment. The dependent variable, average winnings, is passed second.<\/p>\n<p>On a regular bar chart the bar width parameter is intuitively called <code>width<\/code>. On a <code>barh<\/code> chart, somewhat less obviously, it becomes <code>height<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">ax.barh(df.index, df[\"average\"], height=0.65)<\/pre>\n<p>I don&#8217;t want to turn this into a <code>patheffects<\/code> tutorial so I&#8217;ll just link to the documentation <a href=\"https:\/\/matplotlib.org\/stable\/tutorials\/advanced\/patheffects_guide.html\" target=\"_blank\" rel=\"noopener\">here<\/a>. The module is capable of much more but we&#8217;ll use it to create a nice text drop shadow.<\/p>\n<ul>\n<li><code>offset<\/code> defines the shadow&#8217;s horizontal and vertical distance from the foreground text.<\/li>\n<li><code>shadow_rgbFace<\/code> is the shadow&#8217;s color.<\/li>\n<li><code>alpha<\/code> is the shadow&#8217;s transparency. 1.0 means completely opaque.<\/li>\n<\/ul>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">drop_shadow = [path_effects.withSimplePatchShadow(offset=(1.25, -1.25),\r\n                                                  shadow_rgbFace=\"black\",\r\n                                                  alpha=1.0)]<\/pre>\n<p>Since we&#8217;re going to pass a list of strings into <code>set_yticklabels<\/code> (contestant names) it&#8217;s important to first manually set those ticks. This is a non-negotiable step when customizing tick labels! Because if you later tweak the code or underlying data and Matplotlib changes its automatically generated ticks, your labels will then be in the wrong place.<\/p>\n<p>Some of the names are too long to place on a single line so we&#8217;ll replace spaces with newline characters. Notice we pass the previously defined <code>drop_shadow<\/code> as a <code>path_effects<\/code> argument. We&#8217;ll have to pass the argument to other label methods as well.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">ax.set_yticks(df.index)\r\nax.set_yticklabels([item.replace(\" \", \"\\n\") for item in df[\"name\"]], path_effects=drop_shadow)\r\nax.set_ylim(-0.6, 10.6)<\/pre>\n<p>We follow roughly the same process for the x-axis (average daily winnings). Ticks are first set manually and then a list of strings replaces those tick labels.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">x_ticks = range(0, 100000, 20000)\r\nax.set_xticks(x_ticks)\r\nax.set_xticklabels([f\"${n:,}\" if n &gt; 0 else \"0\" for n in x_ticks], path_effects=drop_shadow)\r\nax.set_xlim(0, x_ticks[-1] * 1.03)\r\nax.set_xlabel(\"AVERAGE DAILY WINNINGS\", path_effects=drop_shadow)<\/pre>\n<p>For the plot&#8217;s title I&#8217;d like to have a major heading and a sub-heading below it, each with its own font size. Rather than using <code>set_title<\/code> I&#8217;ll call <code>text<\/code> twice. This is a little more work but it allows for a more customized final product. Don&#8217;t forget the drop shadow here.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">middle_x = mean(ax.get_xlim())\r\nax.text(middle_x, 11.3, \"JEOPARDY! AVERAGE DAILY WINNINGS\", size=15, ha=\"center\", path_effects=drop_shadow)\r\nax.text(middle_x, 10.9, \"TOP 10 WIN STREAKS\", size=12, ha=\"center\", path_effects=drop_shadow)<\/pre>\n<p>Remember we sorted the contestants by their number of games won. We should make that clear to the audience.<\/p>\n<p>We can add the information as text at the tip of each horizontal bar. Use <code>pd.iterrows<\/code> to step through the Dataframe much like you would use <code>enumerate<\/code> from the standard library. Note that only the first bar needs to include the literal text &#8220;Games Won:&#8221;. Each bar below it can simply display a number.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for i, row in df.iterrows():\r\n    if row[\"games_won\"] == df[\"games_won\"].max():\r\n        bar_label = f\"GAMES WON: {row['games_won']}\"\r\n    else:\r\n        bar_label = row[\"games_won\"]\r\n    ax.text(row[\"average\"] - 1000, i, bar_label, size=10, ha=\"right\", va=\"center\")<\/pre>\n<p>Finally, save the figure however you&#8217;d like. I suggest saving in a vectorized <em>SVG<\/em> format if that works for your application. If not, bump up <code>dpi<\/code> to prevent aliasing in the drop shadows. Finer details tend to be lost at Matplotlib&#8217;s default dpi of 100.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">plt.savefig(\"jeopardy_average_win.png\", dpi=200)<\/pre>\n<p>The output:<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_average_win.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-1652 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_average_win.png\" alt=\"\" width=\"1400\" height=\"1600\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_average_win.png 1400w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_average_win-263x300.png 263w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_average_win-896x1024.png 896w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_average_win-768x878.png 768w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2021\/11\/jeopardy_average_win-1344x1536.png 1344w\" sizes=\"auto, (max-width: 1400px) 100vw, 1400px\" \/><\/a><\/p>\n<p>I think the style represents <em>Jeopardy<\/em> well!<\/p>\n<p>As you can see, Matt Amodio averaged a higher daily total than all but James Holzhauer. However, Ken Jennings far out-earned Matt thanks to his unrivaled longevity.<\/p>\n<p>In summary, Amodio holds the 2nd-longest streak as well as the 2nd-largest average win\u2014at least among Hall of Fame members. He can&#8217;t claim to be the best in either category, but his streak represents one of the most well rounded performances in <em>Jeopardy!<\/em> history.<\/p>\n<hr \/>\n<p><strong><a href=\"https:\/\/wollen.org\/misc\/jeopardy_11-19-2021.zip\">Download the data<\/a>.<\/strong><\/p>\n<p><strong>Full code:<\/strong><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\nfrom numpy import mean\r\nimport matplotlib.pyplot as plt\r\nimport matplotlib.patheffects as path_effects\r\n\r\n\r\ndf = pd.read_csv(\"jeopardy_top_winners.csv\")\r\n\r\ndf[\"name\"] = df[\"name\"].str.upper()\r\n\r\ndf.loc[:, \"average\"] = df[\"total_won\"] \/ df[\"games_won\"]\r\n\r\ndf = df.sort_values([\"games_won\", \"average\"]).reset_index(drop=True)\r\n\r\nprint(df)\r\n\r\nplt.style.use(\"jeopardy.mplstyle\")\r\n\r\nfig, ax = plt.subplots()\r\n\r\nax.barh(df.index, df[\"average\"], height=0.65)\r\n\r\ndrop_shadow = [path_effects.withSimplePatchShadow(offset=(1.25, -1.25), shadow_rgbFace=\"black\", alpha=1.0)]\r\n\r\nax.set_yticks(df.index)\r\nax.set_yticklabels([item.replace(\" \", \"\\n\") for item in df[\"name\"]], path_effects=drop_shadow)\r\nax.set_ylim(-0.6, 10.6)\r\n\r\nx_ticks = range(0, 100000, 20000)\r\nax.set_xticks(x_ticks)\r\nax.set_xticklabels([f\"${n:,}\" if n &gt; 0 else \"0\" for n in x_ticks], path_effects=drop_shadow)\r\nax.set_xlim(0, x_ticks[-1] * 1.03)\r\nax.set_xlabel(\"AVERAGE DAILY WINNINGS\", path_effects=drop_shadow)\r\n\r\nmiddle_x = mean(ax.get_xlim())\r\nax.text(middle_x, 11.3, \"JEOPARDY! AVERAGE DAILY WINNINGS\", size=15, ha=\"center\", path_effects=drop_shadow)\r\nax.text(middle_x, 10.9, \"TOP 10 WIN STREAKS\", size=12, ha=\"center\", path_effects=drop_shadow)\r\n\r\nfor i, row in df.iterrows():\r\n    if row[\"games_won\"] == df[\"games_won\"].max():\r\n        bar_label = f\"GAMES WON: {row['games_won']}\"\r\n    else:\r\n        bar_label = row[\"games_won\"]\r\n    ax.text(row[\"average\"] - 1000, i, bar_label, size=10, ha=\"right\", va=\"center\")\r\n\r\nplt.savefig(\"jeopardy_average_win.png\", dpi=200)<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you&#8217;re reading this blog there&#8217;s a good chance you heard about Matt Amodio&#8217;s incredible run on Jeopardy. After winning 38 consecutive games he walked away with over $1.5 million dollars. I thought I&#8217;d take a look at how his<\/p>\n","protected":false},"author":1,"featured_media":1109,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[239],"tags":[164,147,151,153,149,163,105,160,39,155,166,69,144,159,143,156,139,150,157,145,158,24,146,154,126,30,142,25,161,152,148,140,162,141,165],"class_list":["post-837","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-entertainment","tag-alex-trebek","tag-amodio","tag-arthur-chu","tag-austin-rogers","tag-average","tag-average-winnings","tag-bar-chart","tag-barh","tag-code","tag-david-madden","tag-game-show","tag-history","tag-holzhauer","tag-horizontal-bar-chart","tag-james-holzhauer","tag-jason-zuffranieri","tag-jeopardy","tag-jonathan-fisher","tag-julia-collins","tag-ken-jennings","tag-korinna","tag-matplotlib","tag-matt-amodio","tag-matt-jackson","tag-mplstyle","tag-pandas","tag-pyplot","tag-python","tag-record","tag-seth-wilson","tag-streak","tag-style","tag-total-winnings","tag-visualization","tag-wheel-of-fortune"],"_links":{"self":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/837","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=837"}],"version-history":[{"count":26,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/837\/revisions"}],"predecessor-version":[{"id":1653,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/837\/revisions\/1653"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media\/1109"}],"wp:attachment":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media?parent=837"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/categories?post=837"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/tags?post=837"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}