<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Greg Rafferty on Medium]]></title>
        <description><![CDATA[Stories by Greg Rafferty on Medium]]></description>
        <link>https://medium.com/@raffg?source=rss-3c8205f57821------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*DGDK2Cp5IFOPpk4q.jpg</url>
            <title>Stories by Greg Rafferty on Medium</title>
            <link>https://medium.com/@raffg?source=rss-3c8205f57821------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 22 May 2026 19:06:22 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@raffg/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[The Strategic Data Scientist: What 200 Years of Automation Can Teach Us About Thriving in the AI…]]></title>
            <link>https://medium.com/@raffg/the-strategic-data-scientist-what-200-years-of-automation-can-teach-us-about-thriving-in-the-ai-ad01885adbf9?source=rss-3c8205f57821------2</link>
            <guid isPermaLink="false">https://medium.com/p/ad01885adbf9</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[leadership]]></category>
            <category><![CDATA[strategy]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Greg Rafferty]]></dc:creator>
            <pubDate>Wed, 23 Jul 2025 21:18:21 GMT</pubDate>
            <atom:updated>2025-09-30T00:48:06.452Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>The Strategic Data Scientist: What 200 Years of Automation Can Teach Us About Thriving in the AI Era</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*THlVjV0clcxL8TiHMvKb0A.png" /><figcaption>Image generated by Gemini</figcaption></figure><p><em>The data scientists who survive won’t be the ones who code better than ChatGPT — they’ll be the ones who think strategically</em></p><p>This article is an excerpt from my new book about how data scientists can not just survive the AI wave, but use it to level up their careers. If you’d like to learn more, <a href="https://amzn.to/470s3YN">please check it out on Amazon</a>!</p><p>In 2023, ChatGPT wrote its first (successful) SQL queries. By 2024, it was debugging Python code, generating data visualizations, and explaining statistical concepts better than most junior analysts. Today, generative AI can automate much of what we’ve traditionally called “data science work”: the technical execution that used to fill our days and justify our salaries.</p><p>If you’re a data scientist watching AI tools get better at your daily tasks, you’re probably asking the same question that skilled workers have asked for centuries: Will technology make me obsolete?</p><p>History has an answer. And it’s more nuanced than you’d expect. The patterns that emerge from 200 years of technological disruption reveal not just what’s coming, but exactly how to position yourself ahead of it.</p><h3>The Historical Pattern: Automation Eats the Bottom First</h3><p>This isn’t the first time technology has reshaped entire professions. Every major technological revolution follows a predictable pattern: automation starts with the most routine, codifiable tasks and works its way up the skill ladder. Understanding this pattern reveals where we’re headed and where the safe harbor lies.</p><h3>The Industrial Revolution (1760–1840s): From Craft to Factory</h3><p>Before the Industrial Revolution, operating a loom was a demanding craft. A master weaver coordinated hands and feet with perfect rhythm, maintained even tension across threads, and executed complex patterns from memory. Different fibers required different techniques. A single mistake could ruin yards of fabric. Weaving was a precise, responsive art demanding years of practice.</p><p>Then came mechanization. Textile production was broken down into discrete, repeatable steps. The complex craft of weaving was reduced to tending machines that worked faster and more consistently than human hands. Skilled weavers became machine operators overnight.</p><p>The same pattern played out across industries. Blacksmiths who once forged custom tools became factory workers feeding raw materials into standardized processes. The artisan’s comprehensive knowledge was replaced by the factory worker’s specialized efficiency.</p><p>Most data science work today follows this same pattern. We take complex business questions, break them down into standardized analytical steps, and execute them using established tools and frameworks. The craft of data analysis has been industrialized.</p><h3>The Second Industrial Revolution (~1870–1914): Electrification and Mass Production</h3><p>Henry Ford’s innovation wasn’t just building cars, it was breaking car manufacturing into tiny, repeatable tasks that could be performed by workers with minimal training. Complex processes were decomposed into simple, measurable steps. Training time dropped from years to weeks.</p><p>Modern data science teams often work exactly like Ford’s factories. Junior analysts handle data cleaning and basic aggregations. Mid-level data scientists focus on specific modeling techniques. Senior practitioners review outputs and communicate results. Each person handles their piece of the analytical assembly line, but few understand or control the entire process.</p><h3>The Computer Revolution (1940s–1980s): Automating Calculation</h3><p>The computer revolution targeted information processing. Before computers, large organizations employed rooms full of “human computers”: people whose job was to perform calculations and process information by hand. These weren’t unskilled workers; they often had strong mathematical backgrounds and were the data scientists of their era.</p><p>But computers didn’t just make calculation faster, they made it scalable. One machine could do the work of dozens of human computers, working 24/7 without breaks, errors, or salary negotiations. The human computers thought their mathematical skills made them irreplaceable. They were wrong.</p><p>Today’s data scientists often make the same mistake, thinking our technical skills — our ability to write SQL, manipulate data, and build models — make us indispensable. But these skills are becoming as automatable as the mathematical calculations that once required human computers.</p><h3>The Internet and Open-Source Era (1990s–2010s): Access, Scale, and Commoditization</h3><p>The internet democratized access to information and tools. Knowledge that once required expensive training became freely available. Complex technical capabilities were packaged into user-friendly tools that didn’t require deep expertise to use.</p><p>This era was about commoditization. Tasks that once required specialized expertise became accessible to anyone willing to spend a weekend learning the basics. Data analysis, web scraping, and statistical modeling became commoditized skills.</p><p>Most techniques that feel “advanced” in data science today are actually mature, well-documented processes packaged into plug-and-play libraries. Exploratory data analysis, feature engineering, model selection, and performance evaluation all follow established patterns that can be systematized and automated.</p><h3>The Gen AI Leap (2020s–): Automating Thought Templates</h3><p>This brings us to today’s revolution, which is fundamentally different. Previous waves replaced physical labor, calculation, and information processing. Gen AI is targeting the patterns of thought itself.</p><p>For the first time, we have tools that don’t just store knowledge. They mimic expertise. They can take a business question, reason through analytical approaches, generate code, interpret results, and communicate findings. They’re automating the analytical reasoning process that most data scientists consider their core competency.</p><h3>What This Means for Data Scientists Today</h3><p>History doesn’t repeat, but it rhymes. Every wave of technology reshuffles the labor market, but the pattern is consistent. Automation starts with routine work and gradually moves up the skill ladder. What’s different this time is that AI is targeting cognitive work: the patterns of thought we assumed were uniquely human.</p><p>But the workers who thrived weren’t the ones who competed with the machines. They were the ones who learned to work with them strategically.</p><p>The most successful textile workers during the Industrial Revolution weren’t the ones who tried to out-weave the machines. They were the ones who became pattern designers, quality supervisors, and production managers. They moved from executing craft to directing strategy.</p><p>The same principle applies today. The data scientists who will thrive aren’t the ones who will write better SQL than ChatGPT. They’re the ones who will learn to work strategically — asking better questions, making better decisions, and driving better outcomes.</p><h3>What Strategic Data Science Looks Like in Practice</h3><p>During my research into how data scientists can thrive in the age of AI, I saw the same pattern repeat itself again and again: that the skills protecting data scientists from AI automation are identical to the behaviors that differentiate senior individual contributors from their junior colleagues.</p><p>This isn’t a coincidence. The strategic thinking that makes you irreplaceable to AI also makes you promotion-ready. As AI compresses skill requirements upward, the L4 mindset won’t be sustainable at the L4 level, and the L5 way of working won’t cut it for L5 roles.</p><p>Here are four key areas where strategic data scientists consistently outperform their peers:</p><h3>1. Strategic Question Framing and Problem Selection</h3><p>While AI can answer almost any analytical question you give it, it can’t determine which questions are worth asking in the first place. Strategic data scientists understand that the right question is more valuable than the perfect analysis of the wrong question.</p><p><strong>In practice, this means:</strong></p><ul><li>Developing business acumen to identify which problems actually matter</li><li>Learning to scope initiatives that are both impactful and feasible</li><li>Anticipating second-order effects of your recommendations</li></ul><p><strong>Example:</strong> Instead of asking “What are our customer churn rates by segment?” a strategic data scientist asks “Which customer behaviors predict churn early enough for us to intervene, and what interventions are most cost-effective?”</p><p>When everyone can run the analysis, the competitive advantage shifts to understanding which analysis to run.</p><h3>2. Cross-Functional Leadership and Influence</h3><p>The biggest shift from tactical to strategic thinking is moving from answering questions to shaping strategy. Strategic data scientists don’t just analyze what happened, they influence what happens next through leadership without formal authority.</p><p><strong>In practice, this means:</strong></p><ul><li>Developing cross-functional leadership skills to drive initiatives across teams</li><li>Building stakeholder management abilities to create consensus around data-driven decisions</li><li>Executing high-impact projects by working through influence rather than hierarchy</li></ul><p><strong>Example:</strong> Instead of simply reporting that Feature A has higher engagement than Feature B, a strategic data scientist identifies why, proposes how to apply those insights to other features, and leads the cross-functional effort to implement the changes.</p><p>The most successful data scientists understand that impact comes from driving initiatives, not just delivering analyses.</p><h3>3. Strategic Communication and Executive Presence</h3><p>AI can generate charts and summaries, but it can’t tell a story that changes minds. Instead of merely presenting findings, strategic data scientists craft narratives that drive decision-making at the executive level.</p><p><strong>In practice, this means:</strong></p><ul><li>Mastering executive communication styles and preferences</li><li>Framing analytical insights in terms of business impact</li><li>Moving from “here’s what the data shows” to “here’s what we should do about it”</li></ul><p><strong>Example:</strong> Instead of presenting a chart showing declining engagement metrics, a strategic data scientist presents a story: “Our engagement drop correlates with the new feature rollout. Here’s a three-step plan to fix it, including the expected timeline and resource requirements.”</p><p>The most successful data scientists spend as much time crafting their communication as they do running their analyses.</p><h3>4. Intelligent AI Integration and Workflow Optimization</h3><p>Rather than competing with AI, strategic data scientists learn to work with it as a force multiplier. This means understanding how to delegate routine tasks effectively, knowing when to trust AI outputs and when to dig deeper, and maintaining strategic oversight.</p><p><strong>In practice, this means:</strong></p><ul><li>Using AI for data cleaning, initial exploration, and code generation</li><li>Maintaining quality control and strategic direction over AI outputs</li><li>Focusing human effort on problem definition, method selection, and solution design</li></ul><p><strong>Example:</strong> Let AI generate your initial data exploration scripts and basic visualizations, but you determine which patterns are worth investigating, when to test versus model, and how insights connect to business objectives.</p><p>The key is treating AI as a powerful junior analyst that needs clear direction and quality oversight while you focus on the strategic decisions that drive impact.</p><h3>The Path Forward: From Execution to Strategy</h3><p>The transformation from tactical executor to strategic partner isn’t just about learning new skills; it’s about fundamentally changing how you approach your work. The analytical skills you’ve already developed are the perfect foundation for strategic thinking. You already know how to break down complex problems, evaluate evidence, and draw logical conclusions. Now it’s time to apply those same capabilities to business strategy, stakeholder management, and cross-functional leadership.</p><p>The data scientists who will thrive in the next decade won’t be the ones who resist this change. They’ll be the ones who embrace it and use it to do more strategic, more impactful work. They’ll learn to work with AI as a strategic co-pilot, delegating execution while focusing on the higher-level thinking that drives real impact.</p><p>Most importantly, they’ll understand that their value doesn’t come from their ability to write perfect code or build flawless models. It comes from their ability to identify the right problems, ask the right questions, and drive the right outcomes.</p><p>The question isn’t whether AI will change your job. It’s whether you’ll change your job before AI does it for you. The automation wave is here, and it’s time to decide: will you let it sweep you away, or will you learn to ride it to the top?</p><p><em>Did this post ignite your curiosity about becoming a more strategic data scientist? </em><a href="https://amzn.to/470s3YN"><em>Buy</em> The Strategic Data Scientist: Level Up and Thrive in the Age of AI</a><em>. Learn the frameworks, mindsets, and tactics Strategic Data Scientists use to drive impact without managing people; and discover how to work with AI as a strategic co-pilot, not a replacement.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ad01885adbf9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Getting Started with Facebook Prophet]]></title>
            <link>https://medium.com/data-science/getting-started-with-facebook-prophet-20eccb25b06b?source=rss-3c8205f57821------2</link>
            <guid isPermaLink="false">https://medium.com/p/20eccb25b06b</guid>
            <category><![CDATA[forecasting]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[facebook]]></category>
            <category><![CDATA[prophet]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Greg Rafferty]]></dc:creator>
            <pubDate>Wed, 31 Mar 2021 23:18:39 GMT</pubDate>
            <atom:updated>2023-03-23T18:14:08.889Z</atom:updated>
            <content:encoded><![CDATA[<h3>Getting Started with Prophet</h3><h4>Everything you need to to know to build your first model with Meta’s advanced forecasting tool</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YuN9hdPiI3bLlyE1.jpg" /><figcaption>Forecasting Time Series Data with Prophet, available at <a href="https://amzn.to/42xTkOb">https://amzn.to/42xTkOb</a></figcaption></figure><p>This post contains an excerpt from my new book all about Prophet. The book is available for purchase on <a href="https://amzn.to/42xTkOb">Amazon</a>. The full contents of the book are listed at the end of this post!</p><p>If you find this article useful and would like to use Prophet to improve your forecasts, please consider buying the full book at <a href="https://amzn.to/42xTkOb">https://amzn.to/42xTkOb</a>.</p><h3>Building a simple model in Prophet</h3><p>The longest record of direct measurements of CO­₂ in the atmosphere was started in March of 1958 by Charles David Keeling of the Scripps Institution of Oceanography. Keeling was based in La Jolla, California, but had received permission from the <em>National Oceanic and Atmospheric Administration (NOAA)</em> to use their facility located two miles above sea level on the northern slope of Mauna Loa, a volcano on the island of Hawaii, to collect carbon dioxide samples. At that elevation, Keeling’s measurements would be unaffected by local releases of CO₂ such as by nearby factories.</p><p>In 1961, Keeling published the data he had collected thus far, establishing that there was strong seasonal variation in CO₂ levels and that they were rising steadily, a trend that later became known as the <em>Keeling Curve</em>. By May 1974, the NOAA began their own parallel measurements and have continued since then. The keeling curve graph is as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/616/1*WNDPdnYjRymouep5Y1gmGw.png" /><figcaption>The Keeling Curve, showing the concentration of carbon dioxide in the atmosphere</figcaption></figure><p>With its seasonality and increasing trend, this curve makes a good candidate to try out Prophet. This data set contains over 19,000 daily observations across 53 years. The unit of measurement for CO₂ is <em>PPM</em>, or <em>parts per million</em>, a measure of CO₂ molecules per million molecules of air.</p><p>To begin our model, we need to import the necessary libraries, <em>pandas </em>and <em>Matplotlib</em>, and import the Prophet class from the <strong>fbprophet</strong> package.</p><pre>import pandas as pd</pre><pre>import matplotlib.pyplot as plt</pre><pre>from fbprophet import Prophet</pre><p>As input, Prophet always requires a pandas DataFrame with two columns:</p><ul><li><strong>ds</strong>, for datestamp, should be a <strong>datestamp </strong>or <strong>timestamp </strong>column in a format expected by pandas.</li><li><strong>y</strong>, a numeric column containing the measurement we wish to forecast.</li></ul><p>Here, we use pandas to import the data, in this case a <strong>csv </strong>file [Note: This csv can be downloaded at <a href="https://git.io/JYG6T">https://git.io/JYG6T</a>], and then load it into a DataFrame. Note that we also convert the <strong>ds </strong>column to a pandas datetime format, to ensure that Pandas is correctly identifying it as dates and not simply loading it as an alphanumeric string.</p><pre>df = pd.read_csv(‘co2-ppm-daily_csv.csv’)</pre><pre>df[‘date’] = pd.to_datetime(df[‘date’])</pre><pre>df.columns = [‘ds’, ‘y’]</pre><p>If you’re familiar with the scikit-learn (<em>sklearn</em>) package, you’ll feel right at home in Prophet because it was designed to operate in a similar way. Prophet follows the sklearn paradigm of first creating an instance of the model class before calling the <strong>fit </strong>and <strong>predict </strong>methods.</p><pre>model = Prophet()</pre><pre>model.fit(df)</pre><p>In that single <strong>fit </strong>command, Prophet analyzed the data and isolated both the seasonality and trend without requiring us to specify any additional parameters. It has not yet made any future forecast though. To do that, we need to first make a DataFrame of future dates and then call the <strong>predict </strong>method. The <strong>make_future_dataframe </strong>method requires us to specify the number of days we intend to forecast out. In this case, we will choose ten years, or <strong>365 </strong>days times <strong>10</strong>.</p><pre>future = model.make_future_dataframe(periods=365 * 10)</pre><pre>forecast = model.predict(future)</pre><p>At this point, the <strong>forecast </strong>DataFrame contains Prophet’s prediction for CO₂ concentrations going ten years into the future. We will explore that DataFrame in a moment, but first let’s plot the data using Prophet’s <strong>plot </strong>functionality. The <strong>plot </strong>method is built upon Matplotlib; it requires a DataFrame output from the predict method (our forecast DataFrame in this example).</p><p>We’re labeling the axes with the optional <strong>xlabel </strong>and <strong>ylabel </strong>arguments, but just sticking with the default for the optional <strong>figsize </strong>argument. Note that I am also adding a title using raw Matplotlib syntax; because the Prophet plot is built upon Matplotlib, anything you can do to a Matplotlib figure can be performed here as well. Also, don’t be confused by the odd <strong>ylabel </strong>text with the dollar signs; that just tells Matplotlib to use its own TeX-like engine to make the subscript in CO₂.</p><pre>fig = model.plot(forecast, xlabel=’Date’, ylabel=r’CO$_2$ PPM’)</pre><pre>plt.title(‘Daily Carbon Dioxide Levels Measured at Mauna Loa’)</pre><pre>plt.show()</pre><p>The graph is as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*KjSgD10a2O-AokNGZChsZQ.png" /><figcaption>Prophet Forecast</figcaption></figure><p>And that’s it! In those 12 lines of code, we have arrived at our ten year forecast.</p><h3>Interpreting the forecast DataFrame</h3><p>Now, let’s take a look at that <strong>forecast </strong>DataFrame by displaying the first three rows (I’ve transposed it here, in order to better see the column names on the page) and learn how these values were used in the above chart:</p><pre>forecast.head(3).T</pre><p>After running that command, you should see the following table print out:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/683/1*aEw-EhuHhO1ygosVpilrPA.png" /><figcaption>The forecast DataFrame</figcaption></figure><p>The following is a description of each of the columns in the <strong>forecast </strong>DataFrame:</p><ul><li><strong>‘ds’</strong> — Datestamp or timestamp which values in that row pertain to</li><li><strong>‘trend’</strong> — Value of the trend component alone</li><li><strong>‘yhat_lower’</strong> — Lower bound of the uncertainty interval around the final prediction</li><li><strong>‘yhat_upper’</strong> — Upper bound of the uncertainty interval around the final prediction</li><li><strong>‘trend_lower’</strong> — Lower bound of the uncertainty interval around the trend component</li><li><strong>‘trend_upper’</strong> — Upper bound of the uncertainty interval around the trend component</li><li><strong>‘additive_terms’ </strong>— Combined value of all additive seasonalities</li><li><strong>‘additive_terms_lower’</strong> — Lower bound of the uncertainty interval around the additive seasonalities</li><li><strong>‘additive_terms_upper’</strong> — Upper bound of the uncertainty interval around the additive seasonalities</li><li><strong>‘weekly’</strong> — Value of the weekly seasonality component</li><li><strong>‘weekly_lower’</strong> — Lower bound of the uncertainty interval around the weekly component</li><li><strong>‘weekly_upper’ </strong>— Upper bound of the uncertainty interval around the weekly component</li><li><strong>‘yearly’</strong> — Value of the yearly seasonality component</li><li><strong>‘yearly_lower’</strong> — Lower bound of the uncertainty interval around the yearly component</li><li><strong>‘yearly_upper’</strong> — Upper bound of the uncertainty interval around the yearly component</li><li><strong>‘multiplicative_terms’ </strong>— Combined value of all multiplicative seasonalities</li><li><strong>‘multiplicative_terms_lower’</strong> — Lower bound of the uncertainty interval around the multiplicative seasonalities</li><li><strong>‘multiplicative_terms_upper’</strong> — Upper bound of the uncertainty interval around the multiplicative seasonalities</li><li><strong>‘yhat’ </strong>— Final predicted value; a combination of <strong>‘trend’</strong>, <strong>‘multiplicative_terms’</strong>, and <strong>‘additive_terms’</strong></li></ul><p>If the data contains a daily seasonality, then columns for <strong>‘daily’</strong>, <strong>‘daily_upper’</strong>, and <strong>‘daily_lower’</strong> will also be included, following the pattern established with the <strong>‘weekly’</strong> and <strong>‘yearly’</strong> columns. Later chapters will include discussion and examples of both the additive/multiplicative seasonalities and of the uncertainty intervals.</p><p>In the forecast plot above, the black dots represent the actual recorded <strong>y</strong> values we fit on (those in the <strong>df[‘y’]</strong> column) whereas the solid line represents the calculated <strong>yhat </strong>values (the <strong>forecast[‘yhat’]</strong> column). Note that the solid line extends beyond the range of the black dots where we have forecasted into the future. The lighter shading notable around the solid line in the forecasted region represents the uncertainty interval, bound by <strong>forecast[‘yhat_lower’]</strong> and <strong>forecast[‘yhat_upper’]</strong>.</p><p>Now let’s break down that forecast into its components.</p><h3>Understanding components plots</h3><p>In <em>Chapter 1, History and Development of Time Series Forecasting</em>, Prophet was introduced as an additive regression model. <em>Figures 1.4 </em>and <em>1.5</em> showed how individual component curves for the trend and the different seasonalities are added together to create a more complex curve. The Prophet algorithm essentially does this in reverse; it takes a complex curve and decomposes it into its constituent parts. The first step towards greater control of a Prophet forecast is to understand these components so that they can be manipulated individually. Prophet provides a method <strong>plot_components </strong>to visualize these.</p><p>Continuing on with our progress on the Mauna Loa model, plotting the components is as simple as running these commands:</p><pre>fig2 = model.plot_components(forecast)</pre><pre>plt.show()</pre><p>As you can see in the output plot, Prophet has isolated three components in this data set: the <em>trend</em>, a <em>weekly seasonality</em>, and a <em>yearly seasonality</em>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/638/1*eBE-CpWwtUWnbPUpb2iblA.png" /><figcaption>Mauna Loa components plot</figcaption></figure><p>The <em>trend </em>constantly increases but seems to have a steepening slope as time progresses — an acceleration of CO₂ concentration in the atmosphere. The trend line also shows slim uncertainty intervals in the forecasted year. From this curve, we learn that atmospheric CO₂ concentrations were about 320 PPM in 1965. This grew to about 400 by 2015 and we expect about 430 PPM by 2030. However, these exact numbers will vary depending upon the day of the week and the time of year, due to the existence of the seasonality effects.</p><p>The <em>weekly seasonality</em> shows that by days of the week, values will vary by about 0.01 PPM — an insignificant amount and most likely due purely to noise and random chance. Indeed, intuition tells us that carbon dioxide levels (when measured far enough away from human activity, as they are on the high slopes of Mauna Loa) do not care much what day of the week it is and are unaffected by it.</p><p>We will learn in <em>Chapter 4, Seasonality</em>, how to instruct Prophet not to fit a weekly seasonality, as is prudent in this case. In <em>Chapter 10, Uncertainty Intervals</em>, we will learn how to plot uncertainty for seasonality and ensure that a seasonality such as this can be ignored.</p><p>Now looking at the <em>yearly seasonality</em> reveals that carbon dioxide rises throughout the winter and peaks in May or so, while falling in the summer with a trough in October. Measurements of carbon dioxide can be 3 PPM above or 3 PPM below what the trend alone would predict, based upon the time of year. If you refer back to the original data, in the <em>Keeling Curve</em>, you will be reminded that there was a very obvious cyclic nature to the curve, captured with this yearly seasonality.</p><p>As simple as that model was, that is often all you need to make very accurate forecasts with Prophet! We used no additional parameters than the defaults and yet achieved very good results.</p><p>This excerpt is from chapter 2 of <a href="https://amzn.to/373oIcf">Forecasting Time Series Data with Facebook Prophet</a> available now on <a href="https://amzn.to/373oIcf">Amazon</a>. The book has more than 250 pages of examples, lessons, and descriptions of every single aspect of Prophet and more than 10 instructive datasets are provided to help you learn how to perfect your forecasts by demonstrating Prophet functionality from the simple to the advanced with fully working code.</p><p>The full book contains the following chapters:</p><ol><li>The History and Development of Time Series Forecasting</li><li>Getting Started with Facebook Prophet</li><li>Non-Daily Data</li><li>Seasonality</li><li>Holidays</li><li>Growth Modes</li><li>Trend Changepoints</li><li>Additional Regressors</li><li>Outliers and Special Events</li><li>Uncertainty Intervals</li><li>Cross-Validation</li><li>Performance Metrics</li><li>Productionalizing Prophet</li></ol><p>If you enjoyed this Medium post, please consider ordering it here: <a href="https://amzn.to/42xTkOb">https://amzn.to/42xTkOb</a>. If you do read the book, I would be thrilled to hear your thoughts!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=20eccb25b06b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/getting-started-with-facebook-prophet-20eccb25b06b">Getting Started with Facebook Prophet</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[COVID-19 dashboard]]></title>
            <link>https://medium.com/data-science/covid-19-dashboard-b7f8b7c59431?source=rss-3c8205f57821------2</link>
            <guid isPermaLink="false">https://medium.com/p/b7f8b7c59431</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[visualization]]></category>
            <category><![CDATA[covid19]]></category>
            <category><![CDATA[coronavirus]]></category>
            <dc:creator><![CDATA[Greg Rafferty]]></dc:creator>
            <pubDate>Mon, 23 Mar 2020 18:56:09 GMT</pubDate>
            <atom:updated>2020-04-14T18:04:27.042Z</atom:updated>
            <content:encoded><![CDATA[<h4>I built a web-based dashboard using Dash to visualize the pandemic</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/469/1*LZ4HN21Wprt9fr842JDC1g.png" /></figure><blockquote>Stuck behind a paywall? Click <a href="https://towardsdatascience.com/covid-19-dashboard-b7f8b7c59431?source=friends_link&amp;sk=052bfd87662775d68c5e8b15efcc8aff">here</a> to read the full story with a Friend Link!</blockquote><p>I’m Greg Rafferty, a data scientist in the Bay Area. The code for this project is available on my <a href="https://github.com/raffg/covid-19">GitHub </a>and the dashboard is live at <a href="https://covid-19-raffg.herokuapp.com/">https://covid-19-raffg.herokuapp.com/</a>.</p><p>I built a web dashboard using Python and <a href="https://dash.plot.ly/">Dash</a>, with charts made in <a href="https://plot.ly/">Plotly</a>. The data is provided by <a href="https://github.com/CSSEGISandData/COVID-19">Johns Hopkins Center for Systems Science and Engineering</a> and automatically updates to the dashboard nightly at 5:30pm, Pacific time.</p><h3><strong>Focus selection</strong></h3><p>The dashboard can be set on the pandemic globally, or with a focus on either the United States or Europe through the radio buttons on the top:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/501/1*Xxzg8GHy6-kId6IJKo34LA.png" /></figure><p>This button changes the underlying data for each displayed chart to reflect the selected region.</p><p>Each evening at roughly 5pm, Johns Hopkins updates their data source with new cases from the day. My dashboard automatically runs an ETL script to download the new data, process it into the format required by the dashboard, and upload it to Heroku. The headline here declares when the data was most recently updated.</p><h3><strong>Components</strong></h3><p>There are five main components of the dashboard: the indicators, the infections rates for the selected region, the case analysis by sub-region, the infection map, and the trajectory chart.</p><h4><strong>Indicators</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uXCA6XFnFDeDqMvtFXyKrQ.png" /></figure><p>There are four indicators, each consisting of the current value for the indicator, in red, and a percent change from yesterday, in green.</p><ul><li><strong>CUMULATIVE CONFIRMED</strong> is the running total of all cases tested and confirmed in the selected region.</li><li><strong>CURRENTLY ACTIVE </strong>measures only the cases active today. It is calculated as ACTIVE = CONFIRMED — DEATHS — RECOVERED</li><li><strong>DEATHS TO DATE</strong> measures the running total of all COVID-19-related deaths</li><li><strong>RECOVERED CASES</strong> is the number of cases in which the patient is deemed to have recovered from the illness and is no longer infected nor contagious.</li></ul><h4><strong>Infections</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/912/1*9Sp0IFDW50PaS-h0k_CnPA.png" /></figure><p>The infections chart displays the totals for CONFIRMED, ACTIVE, RECOVERED, and DEATHS for the selected region, by date. Hovering the mouse over the chart will reveal the counts for each of these measures on the specific date. Using the mouse, you can zoom in and out or click and drag to select a box to zoom in on. Additionally, hovering over the chart (or any chart on the dashboard) will make visible several control buttons in the top right of the chart. There are slightly different options for each chart, but of particular usefulness is the ability to reset the chart back to original zoom level.</p><h4><strong>Cases by Sub-Region</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/930/1*3ZIMSpSMsdoftMmxJRmdog.png" /></figure><p>The cases graphic displays a line chart by sub-region of either CONFIRMED, ACTIVE, RECOVERED, or DEATHS, selectable with the radio buttons below the chart. If the selected region is Worldwide or Europe, the sub-regions displayed are countries. If the selected region is United Sates, the sub-regions are the states. On hover, the exact count of the selected metric is displayed for the sub-region the mouse is over.</p><p>By default, it displays sub-regions which were of particular interest when this dashboard was created. The dropdown-bar on the bottom allows you to select different sub-regions for display, either countries for the Worldwide and Europe focus or states for the United States focus. Typing in the dropdown-bar will allow you to search for sub-regions.</p><p>As with the other two line charts on this dashboard, clicking on an item in the legend will temporarily remove that item from the chart. Clicking again will add it back. Double-clicking an item will remove all other items and isolate that singular item on the chart. Double-clicking again will add back all items.</p><h4><strong>Infection Map</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/950/1*P4uqqw-yKybLUfVkKfoziA.png" /></figure><p>The infection map features a circular marker over each sub-region. The size of the marker is relative to the square root of the CONFIRMED cases within that sub-region and the color indicates the percentage of those cases which were newly confirmed within the previous 7 days. Essentially, the size of the marker is a measure of how many people have caught the virus within that sub-region since the outbreak began and the color is a measure of how active the virus currently is, with dark red indicating the virus is actively spreading and white indicating that it is more under control. Hovering over a marker will reveal the country name and the exact value of the two measures. As with the other charts, the map is zoomable and dragable. Below the chart is a slider bar controlling the date at which the map displays data. By default it is set for the most recent date available but by dragging to the left you can see the spread of the pandemic through time.</p><h4><strong>Trajectory</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/943/1*blsx6TUhq3rwA9NFIAk03A.png" /></figure><p>This chart displays the trajectory of the pandemic within sub-regions. The x-axis displays the cumulative confirmed count by sub-region and the y-axis displays the count of cases which were confirmed in the previous week. With this visualization, once a sub-region has managed to control the pandemic to some extent, the line should suddenly drop down, as China (green) and South Korea (orange) have in the image. Although date is not on either of the axes, the data is still plotted by date; hovering over any line will display the date on which that data point was recorded. Additionally, the date slider on the bottom also controls this chart; so along with the map, the change throughout time of the trajectories can be inspected.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b7f8b7c59431" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/covid-19-dashboard-b7f8b7c59431">COVID-19 dashboard</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What’s the Most Wonderful Time of the Year? Hint: It’s not what The Economist says]]></title>
            <link>https://medium.com/data-science/whats-the-most-wonderful-time-of-the-year-hint-it-s-not-what-the-economist-says-45d96551b664?source=rss-3c8205f57821------2</link>
            <guid isPermaLink="false">https://medium.com/p/45d96551b664</guid>
            <category><![CDATA[visualization]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[music]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[spotify]]></category>
            <dc:creator><![CDATA[Greg Rafferty]]></dc:creator>
            <pubDate>Thu, 27 Feb 2020 01:45:17 GMT</pubDate>
            <atom:updated>2020-04-14T18:05:10.494Z</atom:updated>
            <content:encoded><![CDATA[<h4>Analyzing Spotify’s valence score with Python and Matplotlib</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Q84rJsamc-P5LujoDatQkA.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@marcelalaskoski?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Marcela Laskoski</a> on <a href="https://unsplash.com/?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><blockquote>Stuck behind the paywall? Click <a href="https://medium.com/@raffg/whats-the-most-wonderful-time-of-the-year-hint-it-s-not-what-the-economist-says-45d96551b664?source=friends_link&amp;sk=f19e71cf528127960d9dfc0a1146b2d4">here</a> to read the full story with a Friend Link!</blockquote><p>I’m Greg Rafferty, a data scientist in the Bay Area. The code for this project is available on my <a href="https://github.com/raffg/spotify_analysis">GitHub</a>.</p><p>In the Feb 8th, 2020 edition of <a href="https://www.economist.com/printedition/2020-02-08">The Economist</a>, the Graphic Detail section briefly discussed an analysis of Spotify data suggesting that July is the happiest month on average (<a href="https://www.economist.com/graphic-detail/2020/02/08/data-from-spotify-suggest-that-listeners-are-gloomiest-in-february">Sad songs say so much:<br>Data from Spotify suggest that listeners are gloomiest in February</a>). I attempted to duplicate their study and came to some different conclusions, and made some new discoveries along the way.</p><h3>The Data</h3><p>There are two sources of data needed for such an analysis. The first is the Top 200 most streamed songs, by day and by country, which Spotify makes available on <a href="https://spotifycharts.com/">spotifycharts.com</a>. Because I didn’t want to select each individual country and date from drop-down menus and manually download the almost-70,000 daily chart csv files, I built a <a href="https://github.com/raffg/spotify_analysis/blob/master/RankingScraper.py">scraper</a> to do it all for me.</p><p>The second set of necessary data is the valence scores for each song in those charts. Spotify makes this data available via their <a href="https://developer.spotify.com/">developer API</a>. To access this data, you’ll need to sign up for credentials <a href="https://developer.spotify.com/dashboard/login">here</a>. Thankfully, Spotify doesn’t make this too difficult though, so you shouldn’t have any problems. I built a second <a href="https://github.com/raffg/spotify_analysis/blob/master/FeatureScraper.py">scraper</a> which goes through each unique song from the Top 200 charts and downloads its feature vector. There are several features available here, but what The Economist used was the valence score, a decimal between 0 and 1 which describes the “happiness” of the song.</p><p>This score was originally developed by a music intelligence and data platform called Echo Nest, which was acquired by Spotify in 2014. A (now dead, but available via the Wayback Machine) <a href="https://web.archive.org/web/20170422195736/http://blog.echonest.com/post/66097438564/plotting-musics-emotional-valence-1950-2013">blog post</a> only has this to say about the score:</p><blockquote>We have a music expert classify some sample songs by valence, then use machine-learning to extend those rules to all of the rest of the music in the world, fine tuning as we go.</blockquote><p>Other features available via the API include tempo, energy, key, mode, and danceability, among others, and it has been speculated that these features play a role in the valence score. At any rate, it’s a bit of a black box how the valence score is arrived at, but it does seem to match up with the songs very well. However, as the training data is highly likely to favor popular music, I wonder if classical, jazz, or non-Western musical styles are not scored as accurately.</p><h3>The Analysis</h3><p>My analysis showed a very similar distribution of data compared with The Economist’s analysis:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*ntkV5NIrJErQLBTsp5f7JA.png" /><figcaption>source: <a href="https://www.economist.com/graphic-detail/2020/02/08/data-from-spotify-suggest-that-listeners-are-gloomiest-in-february">https://www.economist.com/graphic-detail/2020/02/08/data-from-spotify-suggest-that-listeners-are-gloomiest-in-february</a></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/918/1*4RE3WYlXBqdaljA0z-cp1w.png" /></figure><p>Despite the different appearances of our two charts, the shape of the two kernel density estimations is very similar (if you’re not familiar, a kernel density estimation, or KDE, is pretty much just a smooth histogram with the area under the curve summing up to 1). The locations of those key songs along the valence axis also matches up. You can see that on average, Brazilians listen to “happier” music than the rest of the world, and in the United States listeners stream music which on average is less happy than the rest of the world. As we’ll see, the music of Latin America typical scores very high in valence.</p><p>It’s the second chart from The Economist where I see some key differences.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/913/1*u3DgZu_sDNBHXvEOtrcqHA.png" /><figcaption>source: <a href="https://www.economist.com/graphic-detail/2020/02/08/data-from-spotify-suggest-that-listeners-are-gloomiest-in-february">https://www.economist.com/graphic-detail/2020/02/08/data-from-spotify-suggest-that-listeners-are-gloomiest-in-february</a></figcaption></figure><p>First, let’s look at that ten-day moving average chart in the upper right corner. It finds that February displays the lowest average valence and July the highest. Here is what I found:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/936/1*RZOiy8Mg1g0tyBJveVHk-w.png" /></figure><p>In my analysis, December has the highest average valence, followed by August, and then in third place is July. I initially found some very different average valence scores for February as well, and so investigated why our data sets could be different. The Economist used data from January 1st, 2017 (the earliest available on Spotify Charts) until January 29th, 2020 (presumably, when they performed their scraping). I had all of that data, plus almost all of February 2020 as well. Without aggregating by month, as the above plot does, and also not performing a moving average, I saw a much wider variance in the same month from year to year than I expected:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/936/1*OlMzvHsHmrrucTZDg4F7cg.png" /></figure><p>In any given year, I did see December as the highest. However, in 2018 (a particularly sad year?) the summer featured lower valence scores than February. Additionally, the inclusion of 2020 data, which is much higher than the previous years, acted to inflate February’s average across time. Therefore, I chose to exclude 2020 data. Compare these two charts, one with 2020 included and the other without:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/744/1*CETFhcL7c2_uAQQS83_bIw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/744/1*QSfZQrf6_GqZ35yU5wGu5Q.png" /><figcaption>On the left: including Jan/Feb 2020; On the right: excluding Jan/Feb 2020</figcaption></figure><p>This doesn’t change the charts a great deal, however, one key point The Economist noted in their chart was that southern hemisphere New Zealand also experiences a dip in valence in February despite the reversal of their summers and winters compared to the northern hemisphere. When I include February 2020, I see the opposite effect as to what The Economist noticed, but when I exclude February 2020, then I do see the dip; however much less pronounced than what The Economist saw. Following The Economist’s convention of including all available data though, I would include February and, because 2020 was so much happier than previous years, this proves the opposite point to be true. Including this data does, unfortunately, seem somewhat arbitrary to me — what do we call an appropriate cutoff point? Including an equal number of months in each year seems reasonable to me though, so slightly less arbitrary to exclude the 2020 data. I’ll be curious to rerun this analysis at the end of the year and see what it looks like then.</p><p>A key finding of mine though which is most certainly different than what The Economist found is that December is the happiest month, not July as they found.</p><p>I also sorted the countries into continents to look at wider trends. As The Economist found, Latin American countries do indeed stream <em>much </em>happier music than the rest of the world. Also to note is that in every continent except Europe, December is the happiest month; and February the saddest everywhere except Africa and Australia:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/717/1*-_oaqpEVbXfwnFIZrg84gw.png" /></figure><p>Additionally, I looked at mood by day. As I had expected, I found Saturday to be the happiest.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/942/1*3BjTg5eS9qeN7ygzJd3rDw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/942/1*EO9npGi50efXtuPosa-mIg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/942/1*iyiG2OWqdJAprfCvPTz_Ew.png" /></figure><p>I also looked at this chart for just the US and New Zealand. I found Friday to be the saddest day in the United States and Sunday to be the happiest. Any theories why this may be? New Zealand exhibits the behavior I’d probably most expect, with Monday being the saddest and Saturday the happiest.</p><p>Finally, I did spend a bit of time looking into the other features available via the Spotify API and made one last chart, danceability for each country:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/744/1*qzDxZ0MjEluLYEsYx1XH0Q.png" /></figure><p>Here, I found Fridays to feature more danceable music than Mondays, as I was expecting. Furthermore, the ordering of the countries shifts around a lot from the valence chart. For instance, the United States is at the sadder end of the valence spectrum but at the more danceable end in this chart. I also noted that the lowest countries as ranked by danceability are all Asian countries with traditional music styles which do not fit the “standard” western 12-tone, 4-beats-to-a-bar system. I wonder if the algorithm has a tough time predicting danceability when the structure is so different than the majority of training samples.</p><p>I also noted that on Sundays in the Netherlands, their danceability score plummets! Norway and Sweden experience this phenomenon, although to a lesser extent. Religious Sundays may be one explanation for this result, although the Netherlands, Norway, and Sweden don’t seem to me to be particularly more religious than many other countries which don’t exhibit this behavior.</p><p>Just for fun, I looked up the saddest song I know (tragically missing from the Top 200 charts), the 3rd symphony by Henryk Górecki (titled, very appropriately, Symphony of Sorrowful Songs). The 2nd movement features a valence score of just 0.0280, well below Adele’s <em>Make You Feel My Love</em> at 0.0896. This score would place it second-to-last on the valence chart for all 68,000+ songs in the Top 200, just slightly above Tool’s <em>Legion Inoculant</em>, the saddest song in the charts with a valence of 0.0262 (although, in my opinion, this composition by Tool really stretches the definition of “song” beyond reasonableness).</p><p>I also looked up Pharrel Williams’ <em>Happy</em>, expecting to find a peak score, and was disappointed to see that it’s “only” 0.9620. Compare that to the almost comically happy <em>September </em>by Earth, Wind &amp; Fire, with a valence score of 0.982 (the highest in the charts).</p><p>The top 10 happiest songs in the United States’ top 200 rankings:</p><pre><a href="https://open.spotify.com/track/1mqlc0vEP9mU1kZgTi6LIQ">Earth, Wind &amp; Fire - September</a><br><a href="https://open.spotify.com/track/25leEEaz1gIpp7o21Fqyjo">Gene Autry - Here Comes Santa Claus (Right Down Santa Claus Lane)</a><br><a href="https://open.spotify.com/track/5xlS0QkVrSH7ssEbBgBzbM">The Beach Boys - Little Saint Nick - 1991 Remix</a><br><a href="https://open.spotify.com/track/0jqBo5RYn008f4ZY8kPewW">Logic - Indica Badu</a><br><a href="https://open.spotify.com/track/3BUQFs6aFWh7EFNmI8bfL7">Chuck Berry - Johnny B. Goode</a><br><a href="https://open.spotify.com/track/79cuOz3SPQTuFrp8WgftAu">Shawn Mendes - There&#39;s Nothing Holdin&#39; Me Back</a><br><a href="https://open.spotify.com/track/7w87IxuO7BDcJ3YUqCyMTT">Foster The People - Pumped Up Kicks</a><br><a href="https://open.spotify.com/track/7gSQv1OHpkIoAdUiRLdmI6">Tom Petty - I Won&#39;t Back Dow</a>n<br><a href="https://open.spotify.com/track/2PpruBYCo4H7WOBJ7Q2EwM">OutKast - Hey Ya!</a><br><a href="https://open.spotify.com/track/7s25THrKz86DM225dOYwnr">Aretha Franklin - Respect</a></pre><p>And the top 10 saddest songs:</p><pre><a href="https://open.spotify.com/track/48C0O5CXfQdfjUCUhOs1YP">TOOL - Legion Inoculant</a><br><a href="https://open.spotify.com/track/606F3qdYCXtDVtKN53YsuW">Joji - I&#39;LL SEE YOU IN 40</a><br><a href="https://open.spotify.com/track/5e4oAwSsIzkNZxh4fLSKUH">Trippie Redd - RMP</a><br><a href="https://open.spotify.com/track/4czcw3NVLY0of5hTD7OufN">Drake - Days in The East</a><br><a href="https://open.spotify.com/track/3jipFRgLyKK0oJoG1pKicx">Drake - Jaded</a><br><a href="https://open.spotify.com/track/65kp3OFn7JXbCvkm3m2Ui2">Lil Uzi Vert - Two®</a><br><a href="https://open.spotify.com/track/4qE9yOgBNsARadpZTAb6RH">TOOL - Litanie contre la Peur</a><br><a href="https://open.spotify.com/track/7eZOvhHWlB3AcrOuZfTTOA">Russ - Cherry Hill</a><br><a href="https://open.spotify.com/track/6Z4rmc0uujCpl8yXe3yjgI">2 Chainz - Whip (feat. Travis Scott)</a><br><a href="https://open.spotify.com/track/6nI74KsH94IN0J2vp5shdT">Rae Sremmurd - Bedtime Stories (feat. The Weeknd) - From SR3MM</a></pre><p>So, it seems that Andy Williams is correct: December actually is the Most Wonderful Time of the Year (valence score: 0.7240).</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FgFtb3EtjEic%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DgFtb3EtjEic&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FgFtb3EtjEic%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/b01581b5bc0525ec2fd0555e3138a899/href">https://medium.com/media/b01581b5bc0525ec2fd0555e3138a899/href</a></iframe><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=45d96551b664" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/whats-the-most-wonderful-time-of-the-year-hint-it-s-not-what-the-economist-says-45d96551b664">What’s the Most Wonderful Time of the Year? Hint: It’s not what The Economist says</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A/B testing — Is there a better way? An exploration of multi-armed bandits]]></title>
            <link>https://medium.com/data-science/a-b-testing-is-there-a-better-way-an-exploration-of-multi-armed-bandits-98ca927b357d?source=rss-3c8205f57821------2</link>
            <guid isPermaLink="false">https://medium.com/p/98ca927b357d</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[statistics]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Greg Rafferty]]></dc:creator>
            <pubDate>Thu, 23 Jan 2020 00:10:57 GMT</pubDate>
            <atom:updated>2020-04-14T18:05:38.778Z</atom:updated>
            <content:encoded><![CDATA[<h3>A/B testing — Is there a better way? An exploration of multi-armed bandits</h3><h4>Using the algorithms of Epsilon-Greedy, Softmax, UCB, Exp3, and Thompson Sampling</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dDfQU9D_IOhL1XpPjlHW8A.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@_themoi?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Benoit Dare</a> on <a href="https://unsplash.com/?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><blockquote>Stuck behind the paywall? Click <a href="https://medium.com/@raffg/98ca927b357d?source=friends_link&amp;sk=4635341ddef1ec4bc903e4945966d714">here</a> to read the full story with a Friend Link!</blockquote><p>I’m Greg Rafferty, a data scientist in the Bay Area. The code for this project is available on my <a href="https://github.com/raffg/multi_armed_bandit">GitHub</a>.</p><p>In this post, I’ll simulate a traditional A/B test and discuss its shortcomings, then I’ll use Monte Carlo simulations to examine some different multi-armed bandit algorithms, which can alleviate many of the problems with a traditional A/B test. Finally, I’ll discuss the termination criteria for the specific case of Thompson Sampling.</p><h3>Part 1: Traditional A/B testing</h3><p>Websites today are meticulously designed to maximize one or even several goals. Should the “Buy Now!” button be red or blue? What headline attracts the most clicks to that news article? Which version of an advertisement has the highest click-through rate? To determine the optimal answer to these questions, software developers employ A/B tests — a statistically sound technique to compare two different variants, version A and version B. Essentially, they’re trying to determine whether the mean value in the blue distribution below is actually different than the mean value of the red distribution, or is that apparent difference actually just due to random chance?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pXkchqat2jyiWM40RwI-bw.png" /><figcaption>Also, an example of the Central Limit Theorem</figcaption></figure><p>In a traditional A/B test, you start by defining what minimum difference between the versions is meaningful. In the above distributions, version A (usually, the current version) has a mean of 0.01. Let’s say this is a 1% click-through rate, or CTR. In order to change our website to version B, we want to see a minimum of 5% improvement, or a CTR of at least 1.05%. Next, we set our confidence level, the statistical confidence that our observed results are due to a true difference as opposed to random chance. Typically, this is called <strong>alpha </strong>and set to 95%. In order to determine how many observations to collect, we use power analysis to determine the required sample size. If alpha can be thought of as the acceptable rate of making a Type I error (False Positive), <strong>power </strong>can be thought of as the acceptable rate of making a Type II error (False Negative).</p><p>Many statisticians believe a Type I error is 4x as costly as a Type II error. Put another way: Your eCommerce website is currently running fine. You believe you’ve identified a change that will increase sales so you implement the change, only to find out that the change actually hurt the website. This is a <strong>Type I error</strong> and has lost you sales. Now imagine that you consider making a change but decide it won’t improve things, even though in reality it would have, and so you don’t make the change. This is a <strong>Type II error</strong>, and cost you nothing but potential opportunity. So if we set our confidence level to 95%, that means that we’re willing to accept a Type I error in only 5% of our experiments. If a Type I error is 4x as costly as a Type II error, that implies that we set our power to 80%; we’re willing to be conservative and ignore a potentially positive change 20% of the time.</p><p>Version A is what’s currently running. So we have historical data and can calculate a mean CTR and corresponding standard deviation. We’ll need these values for version B though, which doesn’t yet exist. For the mean, we’ll use that 5% improvement value, so mean_b = 1.05 * mean_a. Standard deviation though will need to be estimated. This can be a severe downside to traditional A/B testing when this estimation is difficult. In our case though, we’ll just assume version B will have the same standard deviation as version A. With <strong>sigma </strong>standing in for standard deviation and <strong>d</strong> being the difference between our two means, we’ll need to look up <strong>z-scores</strong> for both <strong>alpha </strong>and <strong>beta</strong>, and calculate our sample size with this equation:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/265/1*akBTvvl4vD2Gqp_47mc7Yw.gif" /></figure><p>With that, we simply run our A/B test until the required sample size is obtained. We randomly show visitors to our site either version A or version B and record the CTR for each version. You then either use a stats package or <a href="https://github.com/raffg/natgeo_instagram_anomaly/blob/master/README.md">t-test calculations and a t-test table</a> to arrive at a <strong>p-value</strong>; if the p-value is less than your alpha, 0.05 in this case, then you can state with 95% confidence that you have observed a true difference between version A and version B, not one due to chance.</p><h4>Drawbacks to the traditional A/B test</h4><p>The greatest drawback to a traditional A/B test is that one version may be vastly inferior to another, and yet you must continue to offer that version to visitors until the test is complete, thus losing sales. As stated earlier, you also must arrive at an estimate of standard deviation for your version B; if your guess is incorrect, you may not collect enough samples and fail to achieve statistical power; that is, even if version B truly is better than version A and even if your experiment demonstrates this fact, you do not have enough samples to declare the difference statistically significant. You’ll be forced into a False Negative.</p><p>It would be great if there was a way to run an A/B test, but not waste time on an inferior version B (or C, D, and E…).</p><h3>Part 2: Multi-Armed Bandits</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/298/1*UsF4w1jgbwVMvzFWVTdkQg.jpeg" /></figure><p>Those old slot machines with the single lever on the side which always take your money — those are called one-armed bandits. Imagine a whole bank of those machines lined up side-by-side, all paying out at different rates and values. This is the idea of a multi-armed bandit. If you’re a gambler who wants to maximize your winnings, you obviously want to play the machine with the highest payout. But you don’t know which machine this is. You need to explore the different machines over time to learn what their payouts are, but you simultaneously want to exploit the highest paying machine. A similar scenario is <a href="https://www.feynmanlectures.caltech.edu/info/exercises/Feynmans_restaurant_problem.html">Richard Feynman’s restaurant problem</a>. Whenever he goes to a restaurant, he wants to order the tastiest dish on the menu, but he has to order everything available to find what is that best dish. This balance of <strong>exploitation</strong>, the desire to choose an action which has payed off well in the past,<strong> </strong>and <strong>exploration</strong>, the desire to try options which may produce even better results, is what multi-armed bandit algorithms were developed for.</p><p>How do they do this? Let’s take a look at several algorithms. I won’t spend too much time discussing the mathematics of these algorithms, but I will link to my Python implementations of each of them on my Github which you can refer to for further details. I’ve used the same notation for each algorithm so the select_arm() and update() functions should fully describe the math.</p><h4>Epsilon-Greedy</h4><p>The <a href="https://github.com/raffg/multi_armed_bandit/blob/master/algorithms/epsilon_greedy.py">Epsilon-Greedy</a> algorithm balances exploitation and exploration fairly basically. It takes a parameter, epsilon, between 0 and 1, as the probability of exploring the options (called arms in multi-armed bandit discussions) as opposed to exploiting the current best variant in the test. For example, say epsilon is set at 0.1. Every time a visitor comes to the website being tested, a number between 0 and 1 is randomly drawn. If that number is greater than 0.1, then that visitor will be shown whichever variant (at first, version A) is performing best. If that random number is less than 0.1, then a random arm out of all available options will be chosen and provided to the visitor. The visitor’s reaction will be recorded (a click or no click, a sale or no sale, etc.) and the success rate of that arm will be updated accordingly.</p><p>There are a few things to consider when evaluating multi-armed bandit algorithms. First, you could look at the probability of selecting the current best arm. Each algorithm takes a bit of time to stabilize and find the best arm, but once stabilization is reached epsilon-Greedy should select the best arm at a rate of (1-epsilon) + epsilon/(number of arms). This is because (1-epsilon)% of the time, it will automatically select the best arm and then the remaining time it will select all arms at an equal rate. For different values of epsilon, this is what the accuracy looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*BbPZ7D-xGl579h4RWCQwsw.png" /></figure><p>In all these trials, I’ve simulated 5 arms with failure/success ratios of [0.1, 0.25, 0.5, 0.75, 0.9]. These values span a far wider range than would typically be seen in a test like this, but they allow the arms to display their behavior after simulating far fewer iterations than would otherwise be required. Each graph is the result of averaging 5000 experiments with a <strong>horizon </strong>of 250 trials.</p><p>Low values of epsilon correspond to less exploration and more exploitation, therefore it takes the algorithm longer to discover which is the best arm but once found, it exploits it at a higher rate. This can be see most clearly with the blue line starting off slowly but then passing the other arms and stabilizing at a higher rate.</p><p>When there are many arms at play, all roughly similar in expected reward, it can be valuable to look at the average reward of an algorithm. The following chart again shows a handful of values for epsilon compared:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/683/1*uNy9p5lUypE7VERsFnrMkw.png" /></figure><p>Both of these approaches, however, focus on how many trials it takes to find the best arm. An evaluation approach which looks at cumulative reward will treat algorithms which focus upfront on learning more fairly.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/680/1*zjGH6638jK-ft1ZM1_LKiw.png" /></figure><p>Clearly, choosing the value of epsilon can matter a great deal and is not trivial. Ideally, you would want a high value (high exploration) when the number of trials is low, but would transition to a low value (high exploitation) once learning is complete and the best arm is known. There is a technique called annealing, which I will not go into too much detail here on, but it is pretty simple. Again, see my <a href="https://github.com/raffg/multi_armed_bandit/blob/master/algorithms/epsilon_greedy_annealing.py">Github code</a> for details, but it basically does exactly what I described: adjust epsilon as the number of trials increases. Using the annealed epsilon-Greedy algorithm and plotting the rate of selecting each arm looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*Ada4UgHPZ1r6xo61t1mGJA.png" /></figure><p>With these (admittedly extreme) values for each arm, the algorithm very quickly settles on arm_0 as the best and selects the remaining arms a fraction of the time.</p><p>One of the greatest advantages of multi-armed bandit approaches is that you can call off the test early if one arm is clearly the winner. In these experiments, each single trial is a Bernoulli trial — the outcome is either success (an ad click, a sale, an email sign-up) or a failure (the user closes the webpage with no action). These trials in aggregate can be represented with Beta distributions. Look at the following graphic. At first, each arm has an equal probability of any outcome. But as more and more trials accumulate, the probability of success of each arm becomes more and more focused on its actual, long-term success probability. Note that the y-axis is the probability density and is increasing in each frame; I’ve omitted it for clarity so just remember that the area under each curve is always exactly 1. As the curves get more narrow, they correspondingly get taller to maintain this constant area.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/657/1*IOKL7gZbZkHY5ceIDSp5ww.gif" /></figure><p>Notice how the peaks of each arm start to center around their actual payout probabilities of [0.1, 0.25, 0.5, 0.75, 0.9]. You can use these distributions to run statistical analyses and stop your experiment early if you reach statistical significance. Another way to look at these changes statically:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/689/1*60kxkQ6AqO1pBZyFIx9sag.png" /></figure><p>This shows a single experiment with a horizon of 1,000,000 trials (as opposed to the average results of 5000 experiments with a horizon of 250), and with more realistic values of [0.01, 0.009, 0.0105, 0.011, 0.015] (in this case, I’ve simulated click through rate, CTR). But what I want to point out is that arm_1, the best arm, is used much more frequently due to the way epsilon-Greedy favors it. The 5% confidence interval (shaded area) around it is much tighter than the other arms. Just as in the gif above, where the best arm has a much tighter and taller bell curve, representing a more precise estimate of the value, this chart shows that using a multi-armed bandit approach allows you to exploit the best arm while still learning about the others, and reach statistical significance earlier than in a traditional A/B test.</p><p><strong>Softmax</strong></p><p>An obvious flaw in epsilon-Greedy is that it explores completely at random. If we have two arms with very similar rewards, we need to explore a lot to learn which is better and so choose a high epsilon. However, if our two arms have vastly different rewards (and we don’t know this when we start the experiment, of course), we would still set a high epsilon and waste a lot of time on the lower paying reward. The <a href="https://github.com/raffg/multi_armed_bandit/blob/master/algorithms/softmax.py">Softmax</a> algorithm (and its <a href="https://github.com/raffg/multi_armed_bandit/blob/master/algorithms/softmax_annealing.py">annealed counterpart</a>) attempt to solve this problem by selecting each arm in the explore phase roughly in proportion to the currently expected reward.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*9rvaAVfM_aOOzQKH7HPGkg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/683/1*9A91f0mSyswcY7NR8NRjUg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/680/1*YgbfGaVpmX3uIf5_FMUl1Q.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*zNit57F1Dmg4rC5GcypBnw.png" /></figure><p>The temperature parameter has a purpose similar to epsilon in the epsilon-Greedy algorithm: it balances the tendency to either explore to exploit. At the extremes, a temperature of 0.0 will <em>always </em>choose the best performing arm. A temperature of infinity will <em>randomly </em>choose any arm.</p><p>What I want you to observe when comparing these algorithms is their different behavior with regards to the explore/exploit balance. This is the crux of the multi-armed bandit problem.</p><h4>UCB1</h4><p>Whereas the Softmax algorithm takes into account the expected value of each arm, it’s certainly plausible by that sheer random chance a poor performing arm will at first have several successes in a row and thus be favored by the algorithm during the exploit phase. They’ll under-explore arms which may have a high payout even though they don’t have enough data to be confident. Thus, it seems reasonable to take into account how much we know about each arm and encourage an algorithm to slightly favor those arms of which we don’t have high confidence in their behavior, so that we can learn more. The Upper Confidence Bound class of algorithms was developed for this purpose; here, I’ll demonstrate two versions, UCB1 and UCB2. They operate similarly.</p><p><a href="https://github.com/raffg/multi_armed_bandit/blob/master/algorithms/ucb1.py">UCB1</a> doesn’t display any randomness at all (you can see in <a href="https://github.com/raffg/multi_armed_bandit/blob/master/algorithms/ucb1.py">my code on Github</a> that I never import the random package at all!). It is fully deterministic, as opposed to both epsilon-Greedy and Softmax. Also, the UCB1 algorithm does not have any parameters needing tuning, which is why the below charts show only one variant. The key to the UCB1 algorithm is its “curiosity bonus”. When selecting an arm, it takes the expected reward of each arm and then adds a bonus which is calculated in inverse proportion to the confidence of that reward. It is optimistic about uncertainty. So lower confidence arms are given a bit of a boost relative to higher confidence arms. This causes the results of the algorithm to swing wildly from trial to trial, especially at the early phases, because each new trial provides more information to the chosen arm and so the other arms will essentially be favored more in the coming rounds.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*yWklIWX2SNJR1ljH8HZicQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*qsK3Zl2kPvjpTAQWasUXUw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/680/1*hA_X8ESKSadjl5-7lnNnAA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*XAQ1GOEiWPUjCJsou5kWjQ.png" /></figure><h4>UCB2</h4><p>The <a href="https://github.com/raffg/multi_armed_bandit/blob/master/algorithms/ucb2.py">UCB2 algorithm</a> is a further development of UCB1. The innovation with UCB2 is to ensure that we trial the same arm for a certain period before trying a new one. This also ensures that in the long term, we periodically take a break from exploiting to re-explore the other arms. UCB2 is a good algorithm to use when rewards are expected to change over time; in the other algorithms, once the best arm is discovered it is heavily favored until the end of the experiment. UCB2 challenges that assumption.</p><p>UCB2 has a parameter, alpha which is effective at tuning the length of UCB2’s periods of favoring certain arms.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*TZJqwIdK9ncK-FYkILHBsA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*3urCtwKyyKj2GIznkDZnsA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*zAhP-6Ku4RLij_TIbzIqXg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/680/1*q_wN__Zyo5B8_W1dL05xaw.png" /></figure><h4>Exp3</h4><p>Finally, we have the <a href="https://github.com/raffg/multi_armed_bandit/blob/master/algorithms/exp3.py">Exp3 algorithm</a>. The UCB class of algorithms is considered the best performing in a <strong>stochastic </strong>setting; ie, the results of each trial are fully random. The <a href="https://github.com/raffg/multi_armed_bandit/blob/master/algorithms/exp3.py">Exp3 algorithm</a> in contrast was developed to handle scenarios where the trials are <strong>adversarial</strong>; that is, we want to consider the possibility that the expected outcome of future trials may be changed by the results of previous trials. A good example of when the Exp3 algorithm might be good is with the stock market. Some investors see a stock listed at a low price per share and buy it up even though its current return isn’t that great, but the very act of buying up the stock in high volume causes its price to surge and its performance as well. The expected earnings of that stock is changing as a result of our algorithm predicting one thing or another. As such, in these experiments here, the Exp3 appears to be performing much worse than the other algorithms. I’m running each trial fully randomly, this is a stochastic setting in which Exp3 was never developed to be strong in.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*qRe2g1LAW7f4OHBV7qiDMw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/683/1*SahTZ9_PK8nJXWTM9r-GtA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/680/1*_r00bEvPXvXI7afJGj70jw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/1*6Cj9DJjxE0GRdLv0hRkkzA.png" /></figure><h4>Comparing Algorithms</h4><p>So now, let’s take a look at all those algorithms together. As you can see, in this short set of trials, UCB2 and epsilon-Greedy look like they’re running together, with UCB2 taking many more opportunities to explore. However, UCB2 is improving slightly more quickly and indeed in longer timeframe, it surpasses epsilon-Greedy. Softmax tends to peak out quite early, indicating that it continues to explore at the expense of exploiting its knowledge of the best arm. UCB1, being an early version of UCB2, trails its more innovative brother as would be expected. Exp3 is an interesting one; it appears to be the lowest performing by far, but this is to be expected as these trials are not what Exp3 is good at. If instead the trial environment were adversarial, we would expect Exp3 to be much more competitive.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/686/1*CECNGD94ZT70-waNkNeSxQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/686/1*_2W7mHXAm7Etx029Wi6iAQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/689/1*Ch5NdPiv6XGUJE5zvy3usw.png" /></figure><p>There’s one algorithm in those charts which we haven’t discussed yet, <a href="https://github.com/raffg/multi_armed_bandit/blob/master/algorithms/thompson_sampling.py"><strong>Thompson Sampling</strong></a>. This algorithm is fully Bayesian. It generates a vector of expected rewards for each arm from a posterior distribution and then updates the distributions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cOiHkFT2fUKHZSk7sAXLtw.png" /></figure><p>The algorithm draws a random number from the changing probability distributions and selects the largest. It simply pulls the lever with the highest expected reward at each trial. Thompson Sampling learns <em>very </em>quickly which is the best arm and <em>heavily </em>favors it going forward, at the expense of exploration. Just look at the uncertainty (the width of the shaded area) in all the other arms! That’s the result of nearly pure exploitation and little exploration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/689/1*NSBVJ79MJXyy_MNX3Igwsw.png" /></figure><h4>When do you end your multi-armed bandit experiment?</h4><p>Because Thompson Sampling draws from arms at the frequency given by their beta distribution, we can use many statistical techniques of uncertainty to know when one arm surges ahead due to superiority as opposed to chance. Google Analytics has developed a sound solution using what they call <a href="https://support.google.com/optimize/answer/7405044?hl=en"><strong>Potential Value Remaining</strong></a>. They first check for three criteria to be met, and then check that a “champion” arm has emerged, and call the experiment complete. Those three criteria are:</p><ul><li>There is active daily traffic on the website</li><li>The experiment has run a minimum of two weeks, in order to cancel out any weekly periodicity.</li><li>Potential Value Remaining is less than 1%</li></ul><p>At that point, once any arm is selected at least 95% of the time, the experiment concludes.</p><p>Potential Value Remaining is also called “regret” in the literature. It describes how much a metric such as CTR may still improve above the leader. When the chance that another arm may beat the leader by only 1% more, that meager improvement isn’t worth continuing the test.</p><p>Potential value remaining in the test is calculated as the 95th percentile of the distribution of (<em>θ</em>ₘₐₓ<em> — θ*) / θ*</em>, where <em>θ</em>ₘₐₓ is the largest value of <em>θ</em> in a row, and <em>θ</em>* is the value of <em>θ</em> for the variation that is most likely to be optimal, with <em>θ</em> being the value drawn from each arm’s beta distribution. As before, for the details of the math please refer to my <a href="https://github.com/raffg/multi_armed_bandit/blob/master/simulation_framework/simulation_framework.py">Github repo</a>, or read the <a href="https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/42550.pdf">original paper</a> published by a Google engineer.</p><p>In the traditional A/B test I described at the beginning of this article, with a confidence interval of 95%, power of 80%, version A CTR of 1%, and hypothesizing a version B CTR of 1.05%, the required sample size was a minimum of <strong>635,829</strong> sample draws from each version. In my experiment, I rounded up to 700,000 draws for each arm.</p><p>When using Thompson Sampling with Google’s termination criteria, I simulated 100 experiments and averaged the results. The algorithm determined that version B was 5% better with an average of <strong>5,357</strong> pulls on the inferior version A arm, and 6,353 pulls on the superior version B.</p><blockquote><strong>In the traditional A/B test, I would have served my customers an inferior version of my website over 600,000 times, whereas Thompson Sampling required just over 5,000 to learn the same thing. That’s 120x fewer mistakes!</strong></blockquote><p>So which bandit is best? It really depends upon your application and needs. They all have their benefits and drawbacks and suitability to specific cases. Epsilon-Greedy and Softmax were early developments in the field and tend not to perform as well as, in particular, the Upper Confidence Bound algorithms. In the realm of web testing, the UCB algorithms do seem to be used most frequently although Thompson Sampling offers the benefits of termination criteria and is the algorithm used by Google Analytics’ Optimize tool. If your context is not stochastic though, you may want to try the Exp3 algorithm which performs better in an adversarial environment.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=98ca927b357d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/a-b-testing-is-there-a-better-way-an-exploration-of-multi-armed-bandits-98ca927b357d">A/B testing — Is there a better way? An exploration of multi-armed bandits</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Forecasting in Python with Facebook Prophet]]></title>
            <link>https://medium.com/data-science/forecasting-in-python-with-facebook-prophet-29810eb57e66?source=rss-3c8205f57821------2</link>
            <guid isPermaLink="false">https://medium.com/p/29810eb57e66</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[visualization]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Greg Rafferty]]></dc:creator>
            <pubDate>Tue, 26 Nov 2019 03:45:00 GMT</pubDate>
            <atom:updated>2023-03-23T18:15:32.689Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to tune and optimize Prophet using domain knowledge to add greater control to your forecasts.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/691/1*HgDtNHGhlgnVg61zf86ZMw.png" /></figure><p><strong>Update: </strong>I’ve written a book about Prophet which has been published by Packt Publishing! The new and updated Second Edition is available for purchase on <a href="https://amzn.to/42xTkOb">Amazon</a>.</p><p>The book covers every detail of using Prophet starting with installation through model evaluation and tuning. Over a dozen datasets have been made available and used to demonstrate Prophet functionality from the simple to the advanced with fully working code. If you enjoy this Medium post, please consider ordering it here: <a href="https://amzn.to/42xTkOb">https://amzn.to/42xTkOb</a>! At more than 250 pages, it covers far more material than can be taught on Medium!</p><p>Thank you so much for supporting my book!</p><blockquote>Stuck behind the paywall? Click <a href="https://towardsdatascience.com/forecasting-in-python-with-facebook-prophet-29810eb57e66?source=friends_link&amp;sk=47194056e7c5185c71d599df762b5257">here</a> to read the full story with a Friend Link!</blockquote><p>I’m Greg Rafferty, a data scientist in the Bay Area. The code for this project is available on my <a href="https://github.com/raffg/prophet_forecasting">GitHub</a>.</p><p>In this post, I’ll explain how to forecast using Facebook’s Prophet and demonstrate a few advanced techniques for handling trend inconsistencies by using domain knowledge. There are a lot of Prophet tutorials floating around the web, but none of them went into any depth about tuning a Prophet model, or about integrating analyst knowledge to help a model navigate the data. I intend to do both of those with this post.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/996/1*Ojg2gNI8FSEVyaP4w7oKgw.jpeg" /><figcaption><a href="https://www.instagram.com/p/BaKEnIPFUq-/">https://www.instagram.com/p/BaKEnIPFUq-/</a></figcaption></figure><p>In a previous story about <a href="https://towardsdatascience.com/forecasting-with-python-and-tableau-dd37a218a1e5">forecasting in Tableau</a>, I used a modification of the ARIMA algorithm to forecast the number of passengers on commercial flights in the United States. The ARIMA approach works decently well with stationary data and when forecasting short time frames, but Facebook’s engineers have built a tool for those cases which ARIMA can’t handle. Prophet is built with its backend in STAN, a probabilistic coding language. This allows Prophet to have many of the advantages offered by Bayesian statistics, including seasonality, the inclusion of domain knowledge, and confidence intervals to add a data-driven estimate of risk.</p><p>I’m going to look at three sources of data to illustrate how to use, and some of the advantages of, Prophet. If you want to follow along, you’ll first need to install Prophet; <a href="https://facebook.github.io/prophet/docs/installation.html#python">Facebook’s documentation provides simple instructions</a>. The <a href="https://github.com/raffg/prophet_forecasting/blob/master/prophet.ipynb">notebook </a>I used for this article provides the full code to build the models discussed.</p><h3>Air Passengers</h3><p>Let’s start out with something simple. The same Air Passengers data from my <a href="https://towardsdatascience.com/forecasting-with-python-and-tableau-dd37a218a1e5">previous article</a>. Prophet requires time series data to have a minimum of two columns: ds which is the time stamp and y which is the values. After loading our data, we need to format it as such:</p><pre>passengers = pd.read_csv(&#39;data/AirPassengers.csv&#39;)</pre><pre>df = pd.DataFrame()<br>df[&#39;ds&#39;] = pd.to_datetime(passengers[&#39;Month&#39;])<br>df[&#39;y&#39;] = passengers[&#39;#Passengers&#39;]</pre><p>With just a few lines, Prophet can make a forecast model every bit as sophisticated as the ARIMA model I built previously. Here, I’m calling Prophet to make a 6-year forecast (frequency is monthly, periods are 12 months/year times 6 years):</p><pre>prophet = Prophet()<br>prophet.fit(df)<br>future = prophet.make_future_dataframe(periods=12 * 6, freq=&#39;M&#39;)<br>forecast = prophet.predict(future)<br>fig = prophet.plot(forecast)<br>a = add_changepoints_to_plot(fig.gca(), prophet, forecast)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*zPdY-d5i_G3xAZ-6hM2bAw.png" /><figcaption>Number of passengers (in the thousands) on commercial airlines in the US</figcaption></figure><p>Prophet has included the original data as the black dots and the blue line is the forecast model. The light blue area is the confidence interval. Using the add_changepoints_to_plot function added the red lines; the vertical dashed lines are changepoints Prophet identified where the trend changed, and the solid red line is the trend with all seasonality removed. This plot format is what I’ll be using throughout this article.</p><p>With that simple case out of the way, let’s move on to more complicated data.</p><h3>Divvy bike share</h3><p><a href="https://www.divvybikes.com/">Divvy </a>is a bike share service in Chicago. I did a project previously where I analysed their data and correlated it with weather information scraped from <a href="https://www.wunderground.com/">Weather Underground</a>. I knew this data exhibited strong seasonality so thought it would be a great demonstration of Prophet’s ability.</p><p>The Divvy data is on a per-ride level so to format the data for Prophet, I aggregated to the daily level and created columns for the mode of the “events” column per day (i.e., the weather conditions: &#39;not_clear&#39;, &#39;rain or snow&#39;, ‘clear&#39;, ‘cloudy&#39;, ‘tstorms&#39;, ‘unknown&#39;), the count of rides, and the mean of temperature.</p><p>Once formatted, let’s look at the number of rides per day:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/710/1*ovvlflewhMW0UJA1GMkXew.png" /></figure><p>So there’s clearly a seasonality to the data, and the trend appears to be increasing with time. With this data set, I want to demonstrate how to add additional regressors, in this case the weather and temperature. Let’s look at the temperature:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/695/1*bB2oEvvuXzdqFKXZf1s95A.png" /></figure><p>It looks a lot like the previous chart, but without the increasing trend. And this similarity makes sense because bicycle riders are going to ride more often when the weather is sunny and warm, so both plots should rise and fall in tandem.</p><p>In order to create a forecast with the addition of another regressor, it is necessary that the additional regressor have data for the forecasted period. For this reason, I’m cutting the Divvy data short a year so I can predict that year with the weather information. You can see I’m also adding Prophet’s default holidays for the US:</p><pre>prophet = Prophet()<br>prophet.add_country_holidays(country_name=&#39;US&#39;)<br>prophet.fit(df[d[&#39;date&#39;] &lt; pd.to_datetime(&#39;2017-01-01&#39;)])<br>future = prophet.make_future_dataframe(periods=365, freq=&#39;d&#39;)<br>forecast = prophet.predict(future)<br>fig = prophet.plot(forecast)<br>a = add_changepoints_to_plot(fig.gca(), prophet, forecast)<br>plt.show()<br>fig2 = prophet.plot_components(forecast)<br>plt.show()</pre><p>The above code block creates the trend plot as described before in the <strong>Air Passengers</strong> section:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*mfaeVj5vJYQsV34dJmpWGA.png" /><figcaption>Divvy trend plot</figcaption></figure><p>And the components plot:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/638/1*lCUGoLhuF9aafnUylTUavg.png" /><figcaption>Divvy component plot</figcaption></figure><p>The components plot consists of 3 sections: the trend, the holidays, and the seasonality. The sum of those 3 components account for the entirety of the model in fact. The trend is simply what the data is showing if you subtract out all of the other components. The holidays plot shows the effect of all of the holidays included in the model. Holidays, as implemented in Prophet, can be thought of as unnatural events when the trend will deviate from the baseline but return once the event is over. Additional regressors, as we’ll explore below, are like holidays in that they cause the trend to deviate from the baseline, except that the trend will stay changed after the event. In this case, the holidays all result in reduced ridership, which again makes sense if we realize that a lot of these riders are commuters to work. The weekly seasonality component shows that ridership is pretty constant throughout the week, but with a steep decline on the weekend. This is the evidence that supports the theory that most riders are commuters. The final thing I want to note is that the yearly seasonality plot is quite wavy. These plots are created with Fourier transforms, essentially stacked sine waves. Clearly, the default in this case has too many degrees of freedom. In order to smooth out the curve, I’ll next create a Prophet model with the yearly seasonality turned off and an additional regressor added to account for it, but with fewer degrees of freedom. I’m also going to go ahead and add in those weather regressors in this model as well:</p><pre>prophet = Prophet(growth=&#39;linear&#39;,<br>                  yearly_seasonality=False,<br>                  weekly_seasonality=True,<br>                  daily_seasonality=False,<br>                  holidays=None,<br>                  seasonality_mode=&#39;multiplicative&#39;,<br>                  seasonality_prior_scale=10,<br>                  holidays_prior_scale=10,<br>                  changepoint_prior_scale=.05,<br>                  mcmc_samples=0<br>                 ).add_seasonality(name=&#39;yearly&#39;,<br>                                    period=365.25,<br>                                    fourier_order=3,<br>                                    prior_scale=10,<br>                                    mode=&#39;additive&#39;)</pre><pre>prophet.add_country_holidays(country_name=&#39;US&#39;)<br>prophet.add_regressor(&#39;temp&#39;)<br>prophet.add_regressor(&#39;cloudy&#39;)<br>prophet.add_regressor(&#39;not clear&#39;)<br>prophet.add_regressor(&#39;rain or snow&#39;)<br>prophet.fit(df[df[&#39;ds&#39;] &lt; pd.to_datetime(&#39;2017&#39;)])<br>future = prophet.make_future_dataframe(periods=365, freq=&#39;D&#39;)<br>future[&#39;temp&#39;] = df[&#39;temp&#39;]<br>future[&#39;cloudy&#39;] = df[&#39;cloudy&#39;]<br>future[&#39;not clear&#39;] = df[&#39;not clear&#39;]<br>future[&#39;rain or snow&#39;] = df[&#39;rain or snow&#39;]<br>forecast = prophet.predict(future)<br>fig = prophet.plot(forecast)<br>a = add_changepoints_to_plot(fig.gca(), prophet, forecast)<br>plt.show()<br>fig2 = prophet.plot_components(forecast)<br>plt.show()</pre><p>The trend plot looks very similar so I’ll only share the components plot:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/638/1*ApBAn7A7v92KfvlXrD8FRQ.png" /><figcaption>Divvy component plot with smooth annual seasonality and weather regressors</figcaption></figure><p>The last year of the trend is upwards in this plot, not downwards as in the previous! This is explained because the last year of data showed lower average temperatures, which reduced ridership more than expected otherwise. We also see that the yearly curve is smoothed out and there’s an additional plot: the extra_regressors_multiplicative plot. This shows the effect of the weather. What we’re seeing is to be expected: ridership is increased in the summer and decreased in winter, and a lot of that variability is accounted for by the weather. I want to see one more thing, just for a demonstration. I ran that above model yet again but this time only included the regressor for rain or snow. Here’s the components plot:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/637/1*PXfXYus_XTYfjJVhSoTtBg.png" /><figcaption>Divvy component plot of just the effect of rain or snow</figcaption></figure><p>This shows that when it’s raining or snowing, there will be about 1400 fewer rides per day than otherwise. Pretty cool, right!?</p><p>Lastly, I wanted to aggregate this dataset by hour to create one more component plot, the daily seasonality. Here’s what that plot looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/637/1*0bYvcUaBn4I3jXql8DEveQ.png" /><figcaption>Divvy component plot for daily seasonality</figcaption></figure><p>As <a href="https://www.ted.com/talks/rives_the_4_a_m_mystery">Rives noted, 4am is the worst possible hour to be awake</a>. Clearly, Chicago’s bicycle riders agree. There’s a local peak just after 8am though: the morning commuters; and a global peak around 6pm: the evening communters. I also see that there’s a small peak just after midnight: I like to think that this is people heading home from the bars. That’s it for Divvy data! Let’s move on to Instagram.</p><h3>Instagram</h3><p>Facebook developed Prophet to analyze its own data. It only seems fair therefore to test out Prophet on a fitting data set. I scoured Instagram for a few accounts exhibiting interesting trends which I wanted to explore and then I scraped the service for all the data for three accounts: <a href="https://www.instagram.com/natgeo/">@natgeo</a>, <a href="https://www.instagram.com/kosh_dp/">@kosh_dp</a>, and <a href="https://www.instagram.com/jamesrodriguez10/">@jamesrodriguez10</a>.</p><h4>National Geographic</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kqEtEU9XmM73oyQqWqWUXQ.jpeg" /><figcaption><a href="https://www.instagram.com/p/B5G_U_IgVKv/">https://www.instagram.com/p/B5G_U_IgVKv/</a></figcaption></figure><p>In 2017, I was working on a <a href="https://public.tableau.com/profile/greg4084#!/vizhome/NationalGeographiconInstagram/Storyboard">project </a>where I noticed an <a href="https://github.com/raffg/natgeo_instagram_anomaly">anomaly</a> in National Geographic’s <a href="https://www.instagram.com/natgeo/">Instagram account</a>. For the month of August in 2016, the number of likes per photo suddenly and inexplicably increased dramatically, but then returned to the baseline as soon as the month was over. I wanted to model this spike as due to a marketing campaign during the month to increase likes, and then see if I could predict the effect of a future marketing campaign.</p><p>Here’s what Natgeo’s likes per post chart looks like. The trend is obviously increasing and there’s also increased variance over time. There are a lot of outliers with dramatically high likes, but there’s that spike in August 2016 where all photos posted during that month had likes which were much higher than the surrounding posts:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/727/1*JytuTCr6bqlN2ycJ3zU3Qw.png" /></figure><p>I don’t want to speculate why this could be, but for the sake of this model let’s just pretend that Natgeo’s marketing department performed some month-long campaign specifically aimed at increasing likes. First, let’s build a model ignoring this fact so we have a baseline to which we can compare:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*NDZwKWnClYTfjitUkzzoEQ.png" /><figcaption>Natgeo likes per photo over time</figcaption></figure><p>Prophet seems to be confused with that spike. It’s attempting to add it to the yearly seasonality component, as can be seen by the August spikes <em>each year</em> in the solid blue line. Prophet wants this to be a recurring event. In order to tell Prophet that something special occurred in 2016 which is not repeating in other years, let’s create a holiday for this month:</p><pre>promo = pd.DataFrame({&#39;holiday&#39;: &quot;Promo event&quot;,<br>                      &#39;ds&#39; : pd.to_datetime([&#39;2016-08-01&#39;]),<br>                      &#39;lower_window&#39;: 0,<br>                      &#39;upper_window&#39;: 31})<br>future_promo = pd.DataFrame({&#39;holiday&#39;: &quot;Promo event&quot;,<br>                      &#39;ds&#39; : pd.to_datetime([&#39;2020-08-01&#39;]),<br>                      &#39;lower_window&#39;: 0,<br>                      &#39;upper_window&#39;: 31})</pre><pre>promos_hypothetical = pd.concat([promo, future_promo])</pre><p>The promo dataframe contains just the August 2016 event, and the promos_hypothetical dataframe contains an additional promo which Natgeo is hypothetically considering for August 2020. When adding a holiday, Prophet allows for a lower window and an upper window, essentially days to include with the holiday event if you, for example, want to include Black Friday with Thanksgiving, or Christmas Eve with Christmas. I’ve added 31 days after the “holiday”, to include the whole month in the event. Here’s the code and the new trend plot. Note that I’m just sending holidays=promo when calling the Prophet object:</p><pre>prophet = Prophet(holidays=promo)<br>prophet.add_country_holidays(country_name=&#39;US&#39;)<br>prophet.fit(df)<br>future = prophet.make_future_dataframe(periods=365, freq=&#39;D&#39;)<br>forecast = prophet.predict(future)<br>fig = prophet.plot(forecast)<br>a = add_changepoints_to_plot(fig.gca(), prophet, forecast)<br>plt.show()<br>fig2 = prophet.plot_components(forecast)<br>plt.show()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*J_hBatKMhiuXRq5xlntbSw.png" /><figcaption>Natgeo likes per photo over time, with a marketing campaign in August 2016</figcaption></figure><p>Fantastic! Now Prophet is not adding that silly August bump annually but is indeed showing a nice spike in just 2016. So now let’s run the model again, but using that promos_hypothetical dataframe, to estimate what would happen if Natgeo were to run an identical campaign in 2020:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*WgwLko2ylkI6k46dQ9z1WQ.png" /><figcaption>Natgeo likes per photo over time with a hypothetical marketing campaign upcoming in 2020</figcaption></figure><p>This demonstrates how to forecast behavior when adding in an unnatural event. Planned merchandise sales could be model this year, for instance. Now let’s move on to the next account.</p><h4>Anastasia Kosh</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C4KYfORbwFkzLRlIT5jyhw.jpeg" /><figcaption><a href="https://www.instagram.com/p/BfZG2QCgL37/">https://www.instagram.com/p/BfZG2QCgL37/</a></figcaption></figure><p><a href="https://www.instagram.com/kosh_dp/">Anastasia Kosh</a> is a Russian photographer who posts whimsical self-portraits to her Instagram and makes music videos for YouTube. We were neighbors on the same street back when I lived in Moscow a few years ago; she had about 10,000 Instagram followers back then but in 2017 her YouTube account went viral in Russia and she has become something of a celebrity among tweens in Moscow. Her Instagram account has grown exponentially and is quickly approaching 1 million followers. This exponential growth seemed like a good challenge for Prophet.</p><p>This is the data we’re going to model:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/719/1*mF-lEcggwSBD1QFVb7OKWQ.png" /></figure><p>It’s the classic hockey stick shape of optimistic growth, except that in this case it’s real! Modelling it with linear growth, the same way we did the other data above, results in unrealistic forecasts:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*ftbQpnhSGgtJqotT4Alp5g.png" /><figcaption>Anastasia Kosh likes per photo over time, with linear growth</figcaption></figure><p>That curve will just keep going on to infinity. Obviously, there’s an upper limit to how many likes a photo on Instagram can get. Theoretically, this would be equal to the number of unique accounts on the service. But realistically, not every account will see, nor like, the photo. This is where a little bit of domain knowledge from the analyst will come in handy. I decided to model this with logistic growth, which requires that Prophet be told a ceiling (Prophet calls it a cap) and a floor:</p><pre>cap = 200000<br>floor = 0<br>df[&#39;cap&#39;] = cap<br>df[&#39;floor&#39;] = floor</pre><p>Through my own knowledge of Instagram and a little bit of trial and error, I decided upon the ceiling of 200,000 likes, and a floor of 0 likes. It’s important to note that Prophet does allow these values to be defined as functions of time, so they needn’t be constant. In this case, constant values were exactly what I needed:</p><pre>prophet = Prophet(growth=&#39;logistic&#39;,<br>                  changepoint_range=0.95,<br>                  yearly_seasonality=False,<br>                  weekly_seasonality=False,<br>                  daily_seasonality=False,<br>                  seasonality_prior_scale=10,<br>                  changepoint_prior_scale=.01)<br>prophet.add_country_holidays(country_name=&#39;RU&#39;)<br>prophet.fit(df)<br>future = prophet.make_future_dataframe(periods=1460, freq=&#39;D&#39;)<br>future[&#39;cap&#39;] = cap<br>future[&#39;floor&#39;] = floor<br>forecast = prophet.predict(future)<br>fig = prophet.plot(forecast)<br>a = add_changepoints_to_plot(fig.gca(), prophet, forecast)<br>plt.show()<br>fig2 = prophet.plot_components(forecast)<br>plt.show()</pre><p>I defined the growth to be logistic, turned off all seasonality (there didn’t appear to be much of it in my plots), and adjusted a few of the tuning parameters. I also added the default holidays for Russia, as that is where the majority of Anastasia’s followers are located. When calling the .fit method on the df, Prophet sees the cap and floor columns and knows to include them in the model. It’s very important though that when you create your forecast dataframe, you add these columns to it (that’s the future dataframe in the code block above). We’ll walk through this again in the next section. But now our trend plot looks a lot more realistic!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*5nGOfwXPI735WO0PWbRdaQ.png" /><figcaption>Anastasia Kosh likes per photo over time, with logistic growth</figcaption></figure><p>Finally, let’s look at our last example.</p><h4>James Rodríguez</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dRUZaChxRUXyW5lSm9Q7Uw.jpeg" /><figcaption><a href="https://www.instagram.com/p/BySl8I7HOWa/">https://www.instagram.com/p/BySl8I7HOWa/</a></figcaption></figure><p><a href="https://www.instagram.com/jamesrodriguez10/">James Rodríguez</a> is a Colombian soccer player who was a standout performer in both the 2014 and 2018 World Cups. His Instagram account has had steady growth since its inception; but while working on a <a href="https://towardsdatascience.com/the-top-50-most-followed-instagrammers-visualized-134ca4788938">previous analysis</a>, I noticed that during the two World Cups his account saw sudden and lasting spikes in followers. In contrast to the spikes in National Geographic’s account, which could be modeled as a holiday, Rodríguez’s growth did not return to the baseline after the two tournaments but redefined a new baseline. This is fundamentally different behavior and will require a different modelling approach to capture it.</p><p>This is what James Rodríguez’s’s likes per photo looks like throughout the account lifetime:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/727/1*EsWsQzJdPt_wrnioIaI40Q.png" /></figure><p>This is going to be difficult to model cleanly with only the techniques we’ve used so far in this tutorial. He experienced an increase in the trend baseline during the first World Cup in the summer of 2014, and then a spike, and potentially a changed baseline, during the second World Cup in the summer of 2018. Modelling this behavior with the default model doesn’t quite work:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*XqcEgdDWpTZ66XCq_LqUaA.png" /><figcaption>James Rodríguez likes per photo over time</figcaption></figure><p>It’s not a <em>terrible </em>model; it just doesn’t neatly model the behavior around those two World Cup tournaments. If, as we did with Anastasia Kosh’s data above, we model those tournaments as holidays, we do see an improvement in the model:</p><pre>wc_2014 = pd.DataFrame({&#39;holiday&#39;: &quot;World Cup 2014&quot;,<br>                      &#39;ds&#39; : pd.to_datetime([&#39;2014-06-12&#39;]),<br>                      &#39;lower_window&#39;: 0,<br>                      &#39;upper_window&#39;: 40})<br>wc_2018 = pd.DataFrame({&#39;holiday&#39;: &quot;World Cup 2018&quot;,<br>                      &#39;ds&#39; : pd.to_datetime([&#39;2018-06-14&#39;]),<br>                      &#39;lower_window&#39;: 0,<br>                      &#39;upper_window&#39;: 40})</pre><pre>world_cup = pd.concat([wc_2014, wc_2018])</pre><pre>prophet = Prophet(yearly_seasonality=False,<br>                  weekly_seasonality=False,<br>                  daily_seasonality=False,<br>                  holidays=world_cup,<br>                  changepoint_prior_scale=.1)<br>prophet.fit(df)<br>future = prophet.make_future_dataframe(periods=365, freq=&#39;D&#39;)<br>forecast = prophet.predict(future)<br>fig = prophet.plot(forecast)<br>a = add_changepoints_to_plot(fig.gca(), prophet, forecast)<br>plt.show()<br>fig2 = prophet.plot_components(forecast)<br>plt.show()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*QIrOGNtBsINwUhSHUMb-cQ.png" /><figcaption>James Rodríguez likes per photo over time, with holidays added for the World Cups</figcaption></figure><p>I still don’t like how slow the model is to adapt to the changed trendline, especially around the 2014 World Cup. It’s just <em>too </em>smooth of a transition. By adding additional regressors though, we can force Prophet to consider an abrupt change.</p><p>In this case, I’m defining two periods for each tournament, during and after. Modelling it this way assumes that before the tournament, there will be a certain trend line, during the tournament there will be a linear change to that trend line, and after the tournament, there will be yet another change. I define these periods as either 0 or 1, on or off, and let Prophet train itself on the data to learn the magnitudes:</p><pre>df[&#39;during_world_cup_2014&#39;] = 0<br>df.loc[(df[&#39;ds&#39;] &gt;= pd.to_datetime(&#39;2014-05-02&#39;)) &amp; (df[&#39;ds&#39;] &lt;= pd.to_datetime(&#39;2014-08-25&#39;)), &#39;during_world_cup_2014&#39;] = 1<br>df[&#39;after_world_cup_2014&#39;] = 0<br>df.loc[(df[&#39;ds&#39;] &gt;= pd.to_datetime(&#39;2014-08-25&#39;)), &#39;after_world_cup_2014&#39;] = 1</pre><pre>df[&#39;during_world_cup_2018&#39;] = 0<br>df.loc[(df[&#39;ds&#39;] &gt;= pd.to_datetime(&#39;2018-06-04&#39;)) &amp; (df[&#39;ds&#39;] &lt;= pd.to_datetime(&#39;2018-07-03&#39;)), &#39;during_world_cup_2018&#39;] = 1<br>df[&#39;after_world_cup_2018&#39;] = 0<br>df.loc[(df[&#39;ds&#39;] &gt;= pd.to_datetime(&#39;2018-07-03&#39;)), &#39;after_world_cup_2018&#39;] = 1</pre><p>Note where I’m updating the future dataframe to include these “holiday” events below:</p><pre>prophet = Prophet(yearly_seasonality=False,<br>                  weekly_seasonality=False,<br>                  daily_seasonality=False,<br>                  holidays=world_cup,<br>                  changepoint_prior_scale=.1)</pre><pre>prophet.add_regressor(&#39;during_world_cup_2014&#39;, mode=&#39;additive&#39;)<br>prophet.add_regressor(&#39;after_world_cup_2014&#39;, mode=&#39;additive&#39;)<br>prophet.add_regressor(&#39;during_world_cup_2018&#39;, mode=&#39;additive&#39;)<br>prophet.add_regressor(&#39;after_world_cup_2018&#39;, mode=&#39;additive&#39;)</pre><pre>prophet.fit(df)<br>future = prophet.make_future_dataframe(periods=365)</pre><pre>future[&#39;during_world_cup_2014&#39;] = 0<br>future.loc[(future[&#39;ds&#39;] &gt;= pd.to_datetime(&#39;2014-05-02&#39;)) &amp; (future[&#39;ds&#39;] &lt;= pd.to_datetime(&#39;2014-08-25&#39;)), &#39;during_world_cup_2014&#39;] = 1<br>future[&#39;after_world_cup_2014&#39;] = 0<br>future.loc[(future[&#39;ds&#39;] &gt;= pd.to_datetime(&#39;2014-08-25&#39;)), &#39;after_world_cup_2014&#39;] = 1</pre><pre>future[&#39;during_world_cup_2018&#39;] = 0<br>future.loc[(future[&#39;ds&#39;] &gt;= pd.to_datetime(&#39;2018-06-04&#39;)) &amp; (future[&#39;ds&#39;] &lt;= pd.to_datetime(&#39;2018-07-03&#39;)), &#39;during_world_cup_2018&#39;] = 1<br>future[&#39;after_world_cup_2018&#39;] = 0<br>future.loc[(future[&#39;ds&#39;] &gt;= pd.to_datetime(&#39;2018-07-03&#39;)), &#39;after_world_cup_2018&#39;] = 1</pre><pre>forecast = prophet.predict(future)<br>fig = prophet.plot(forecast)<br>a = add_changepoints_to_plot(fig.gca(), prophet, forecast)<br>plt.show()<br>fig2 = prophet.plot_components(forecast)<br>plt.show()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*hd9iY1OipkeNbuy5HNlZ6g.png" /><figcaption>James Rodríguez likes per photo over time, with additional regressors</figcaption></figure><p>Here, the blue line is what we should be looking at. The red line shows just the trend, with the influence of the additional regressors and holidays subtracted out. Look how the blue trend line takes sharp jumps during the World Cups. That’s exactly the behavior our domain knowledge tells us would happen! After Rodríguez scored his first World Cup goal, suddenly thousands of new followers arrived on his account. Let’s take a look at the component plot, just to see what specific effect of these additional regressors:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/639/1*DQKXAjKsP9peJjtTkOYv_w.png" /><figcaption>James Rodríguez component plot for the World Cup regressors</figcaption></figure><p>This tells us that in 2013 and the beginning of 2014, the World Cup had no effect on Rodríguez’s likes per photo. During the 2014 World Cup, there was a dramatic uptick in his average like per photo which continued after the tournament was over (this can be explained because he gained so many active followers during the event). There was a similar, but less dramatic, event during the 2018 World Cup, presumably because by this point there weren’t as many soccer fans left to discover his account and follow him.</p><p>Thanks for sticking around for this whole post! I hope you now understand how to use holidays, linear vs. logistic growth rates, and additional regressors to enrich your Prophet forecasts significantly. Facebook has built an incredibly valuable tool with Prophet, making what was once a very difficult exercise of probabilistic forecasting into a simple set of parameters with enormous latitude for tuning. Good luck with your forecasting!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=29810eb57e66" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/forecasting-in-python-with-facebook-prophet-29810eb57e66">Forecasting in Python with Facebook Prophet</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Basic NLP on the Texts of Harry Potter: Sentiment Analysis]]></title>
            <link>https://medium.com/data-science/basic-nlp-on-the-texts-of-harry-potter-sentiment-analysis-1b474b13651d?source=rss-3c8205f57821------2</link>
            <guid isPermaLink="false">https://medium.com/p/1b474b13651d</guid>
            <category><![CDATA[nlp]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[harry-potter]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Greg Rafferty]]></dc:creator>
            <pubDate>Fri, 21 Dec 2018 17:45:40 GMT</pubDate>
            <atom:updated>2019-05-22T01:13:06.179Z</atom:updated>
            <content:encoded><![CDATA[<h3>Sentiment Analysis on the Texts of Harry Potter</h3><h4>With bonus tutorial of Matplotlib advanced features!</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GvLYe9GZvpkp_T9JwUbZLA.jpeg" /></figure><p>I’m Greg Rafferty, a data scientist in the Bay Area. You can check out the code for this project on my <a href="https://github.com/raffg/harry_potter_nlp">github</a>. Feel free to contact me with any questions!</p><p>In this series of posts, I’m looking at a few handy NLP techniques, through the lens of Harry Potter. Previous posts in this series on basic NLP looked at <a href="https://towardsdatascience.com/basic-nlp-on-the-texts-of-harry-potter-topic-modeling-with-latent-dirichlet-allocation-f3c00f77b0f5">Topic Modeling with Latent Dirichlet Allocation</a>, <a href="https://towardsdatascience.com/regex-on-the-texts-of-harry-potter-96b8a3878303">Regular Expressions</a>, and <a href="https://towardsdatascience.com/text-summarization-on-the-books-of-harry-potter-5e9f5bf8ca6c">text summarization</a>.</p><h4>What is sentiment analysis?</h4><p>In a <a href="https://towardsdatascience.com/basic-nlp-on-the-texts-of-harry-potter-topic-modeling-with-latent-dirichlet-allocation-f3c00f77b0f5">previous post</a> I looked at topic modeling, which is an NLP technique to learn the subject of a given text. Sentiment analysis exists to learn <em>what was said</em> about that topic — was it good or bad? With the growing use of the internet in our daily lives, vast amounts of unstructured text is being published every second of every day, in blog posts, forums, social media, and review sites, to name a few. Sentiment analysis systems can take this unstructured data and automatically add structure to it, capturing the public’s opinion about products, services, brands, politics, etc. This data holds immense value in the fields of marketing analysis, public relations, product reviews, net promoter scoring, product feedback, and customer service, for example.</p><p>I’ve been demonstrating a lot of these NLP tasks using the text of Harry Potter. The books are rich in emotionally charged experiences that the reader can viscerally feel. Can a computer capture that feeling? Let’s take a look.</p><h4>VADER</h4><p>I used C.J. Hutto’s <a href="https://github.com/cjhutto/vaderSentiment">VADER</a> package to extract the sentiment of each book. VADER, which stands for <strong>V</strong>alence <strong>A</strong>ware <strong>D</strong>ictionary and s<strong>E</strong>ntiment <strong>R</strong>easoning, is a lexicon and rule-based tool that is specifically tuned to social media. Given a string of text, it outputs a decimal between 0 and 1 for each of negativity, positivity, and neutrality for the text, as well as a compound score from -1 to 1 which is an aggregate measure.</p><p>A complete description of the development, validation, and evaluation of the VADER package can be read in <a href="http://comp.social.gatech.edu/papers/icwsm14.vader.hutto.pdf">this paper</a>, but the gist is that the package’s authors first constructed a list of lexical features correlated with sentiment and then combined the list with some rules that describe how the grammatical structure of a phrase will intensify or diminish the sentiment. When tested against human raters, VADER outperforms with accuracy scores of 96% to 84%.</p><p>VADER works best on short texts (a couple sentences at most), and applying it to an entire chapter at once resulted in extreme and largely worthless scores. Instead, I looped over each sentence individually, got the VADER scores, and then took an average of all sentences in a chapter.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1007/1*jS1UpDsEHWqQrEuCkO-TcA.png" /><figcaption>Goblet of Fire, Chapter 16: Harry discovers the missionary position</figcaption></figure><p>By plotting at the VADER compound score for each chapter of each book, we can clearly mark events in the books. The three greatest spikes in that chart above are Harry being chosen by the Goblet of Fire around chapter 70 of the series, Cedric Diggory’s death at about chapter 88, and Dumbledore’s death at about chapter 160.</p><p>Here’s the code to produce that chart (<a href="https://github.com/raffg/harry_potter_nlp/blob/master/sentiment_analysis.ipynb">the full notebook is available on my Github</a>). The data exists in a dictionary with each book’s title as a key; the value for each book is another dictionary with each chapter number as a key. The value for each chapter is a tuple consisting of the chapter title and the chapter text. I defined a function to calculate the moving average of the data, which essentially smooths out the curve a bit and makes it easier to see long multi-chapter arcs throughout the stories. In order to plot each book as a different color, I created a dictionary called book_indices with each book’s title as the key and the values being a 2-element tuple of the book’s starting chapter number and ending chapter number (as if all the books were concatenated with chapters numbered sequentially throughout the entire series). I then plotted the story arc in segments based upon their chapter numbers.</p><pre>import matplotlib.pyplot as plt</pre><pre># Use FiveThirtyEight style theme<br>plt.style.use(&#39;fivethirtyeight&#39;)</pre><pre># Moving Average function used for the dotted line<br>def movingaverage(interval, window_size):<br>    window = np.ones(int(window_size))/float(window_size)<br>    return np.convolve(interval, window, &#39;same&#39;)</pre><pre>length = sum([len(hp[book]) for book in hp])<br>x = np.linspace(0, length - 1, num=length)<br>y = [hp[book][chapter][2][&#39;compound&#39;] for book in hp for chapter in hp[book]]</pre><pre>plt.figure(figsize=(15, 10))<br>for book in book_indices:<br>    plt.plot(x[book_indices[book][0]: book_indices[book][1]],<br>             y[book_indices[book][0]: book_indices[book][1]],<br>             label=book)<br>plt.plot(movingaverage(y, 10), color=&#39;k&#39;, linewidth=3, linestyle=&#39;:&#39;, label = &#39;Moving Average&#39;)<br>plt.axhline(y=0, xmin=0, xmax=length, alpha=.25, color=&#39;r&#39;, linestyle=&#39;--&#39;, linewidth=3)<br>plt.legend(loc=&#39;best&#39;, fontsize=15)<br>plt.title(&#39;Emotional Sentiment of the Harry Potter series&#39;, fontsize=20)<br>plt.xlabel(&#39;Chapter&#39;, fontsize=15)<br>plt.ylabel(&#39;Average Sentiment&#39;, fontsize=15)<br>plt.show()</pre><p>I also made this same chart using the <a href="https://textblob.readthedocs.io/en/dev/_modules/textblob/en/sentiments.html">TextBlob Naive Bayes and Pattern analyzers</a> with worse results (see the <a href="https://github.com/raffg/harry_potter_nlp/blob/master/sentiment_analysis.ipynb">Jupyter notebook</a> on my Github for these charts). The Naive Bayes model was trained on movie reviews which must not translate well to the Harry Potter universe. The Pattern analyzer worked much better (almost as well as VADER); it is based on the <a href="https://www.clips.uantwerpen.be/pattern">Pattern</a> library, a rule-based model very similar to VADER.</p><h4>Emotion Lexicon</h4><p>I also looked at emotions by using a <a href="http://saifmohammad.com/WebPages/NRC-Emotion-Lexicon.htm">lexicon</a> created by the National Research Council of Canada of over 14,000 words, each scored as either associated or not-associated with any of two sentiments (negative, positive) or eight emotions (anger, anticipation, disgust, fear, joy, sadness, surprise, trust). They kindly provided me access to the lexicon, and I wrote up a Python script which loops over each word in a chapter, looks it up in the lexicon, and outputs whichever emotions the word was associated with. Each chapter was then assigned a score for each emotion corresponding to a ratio of how many words associated with that emotion the chapter contains compared to the total word count in the chapter (this basically normalizes the scores).</p><p>Here are plots of the ‘Anger’ and ‘Sadness’ sentiments. I find it interesting that anger always exists with sadness, but sadness can sometimes exist without anger:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1hG359fJ1v6vgNz6yrsWoA.png" /><figcaption>Wow, Voldemort. You really pissed off Harry when you killed the adults in his life</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FvG8LR1q8plDy_EV_SQwOA.png" /><figcaption>Those mood swings hit hard during puberty</figcaption></figure><p>Again, take a look at the <a href="https://github.com/raffg/harry_potter_nlp/blob/master/sentiment_analysis.ipynb">Jupyter notebook</a> on my Github to see detailed charts for all sentiments. Here’s a condensed version:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vr_4k374G2wWnbRQIV-hkQ.png" /></figure><p>Let’s see how I made all those subplots:</p><pre>length = sum([len(hp[book]) for book in hp])<br>x = np.linspace(0, length - 1, num=length)</pre><pre>fig, ax = plt.subplots(4, 3, figsize=(15, 15), facecolor=&#39;w&#39;, edgecolor=&#39;k&#39;)<br>fig.subplots_adjust(hspace = .5, wspace=.1)<br>fig.suptitle(&#39;Sentiment of the Harry Potter series&#39;, fontsize=20, y=1.02)<br>fig.subplots_adjust(top=0.88)</pre><pre>ax = ax.ravel()</pre><pre>for i, emotion in enumerate(emotions):<br>    y = [hp_df.loc[book].loc[hp[book][chapter][0]][emotion] for book in hp for chapter in hp[book]]<br>    for book in book_indices:<br>        ax[i].plot(x[book_indices[book][0]: book_indices[book][1]],<br>                 y[book_indices[book][0]: book_indices[book][1]],<br>                 label=book, linewidth=2)</pre><pre>ax[i].set_title(&#39;{} Sentiment&#39;.format(emotion.title()))<br>    ax[i].set_xticks([])</pre><pre>fig.legend(list(hp), loc=&#39;upper right&#39;, fontsize=15, bbox_to_anchor=(.85, .2))<br>fig.tight_layout()<br>fig.delaxes(ax[-1])<br>fig.delaxes(ax[-2])<br>plt.show()</pre><p>But it really becomes interesting to see how all the sentiments compare to each other. Overlaying 10 lines with this much variance quickly became a mess, so I again used the moving average:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dbspkFfm2IWXpgx3oauLoA.png" /></figure><p>It’s interesting to see contradicting emotions acting counter to each other, most obviously the pink and brown lines above for ‘Positive’ and ‘Negative’ sentiment. Note that, due to the moving average window size of 20 data points, the first 10 and last 10 chapters have been left off the plot.</p><p>I removed the y-axis because those numbers are meaningless to us (mere decimals: the ratio of words of that emotion to total words in the chapter). I also removed the horizontal and vertical chart lines to clean up the plot. I don’t particularly care to mark regular chapter numbers but I do want to mark the books; therefore, I added those vertical dotted lines. The legend has been reversed in this plot, which isn’t really necessary for readability or anything but I did it for consistency with the area and column charts coming up next.</p><p>Here’s how I made it:</p><pre># use the Tableau color scheme of 10 colors<br>tab10 = matplotlib.cm.get_cmap(&#39;tab10&#39;)</pre><pre>length = sum([len(hp[book]) for book in hp])<br>window = 20</pre><pre># use index slicing to remove data points outside the window<br>x = np.linspace(0, length - 1, num=length)[int(window / 2): -int(window / 2)]</pre><pre>fig = plt.figure(figsize=(15, 15))<br>ax =fig.add_subplot(1, 1, 1)</pre><pre># Loop over the emotions with enumerate in order to track colors<br>for c, emotion in enumerate(emotions):<br>    y = movingaverage([hp_df.loc[book].loc[hp[book][chapter][0]][emotion] for book in hp for chapter in hp[book]], window)[int(window / 2): -int(window / 2)]<br>    plt.plot(x, y, linewidth=5, label=emotion, color=(tab10(c)))<br>    <br># Plot vertical lines marking the books<br>for book in book_indices:<br>    plt.axvline(x=book_indices[book][0], color=&#39;black&#39;, linewidth=2, linestyle=&#39;:&#39;)<br>plt.axvline(x=book_indices[book][1], color=&#39;black&#39;, linewidth=2, linestyle=&#39;:&#39;)</pre><pre>plt.legend(loc=&#39;best&#39;, fontsize=15, bbox_to_anchor=(1.2, 1))<br>plt.title(&#39;Emotional Sentiment of the Harry Potter series&#39;, fontsize=20)<br>plt.ylabel(&#39;Relative Sentiment&#39;, fontsize=15)</pre><pre># Use the book titles for X ticks, rotate them, center the left edge<br>plt.xticks([(book_indices[book][0] + book_indices[book][1]) / 2 for book in book_indices],<br>           list(hp),<br>           rotation=-30,<br>           fontsize=15,<br>           ha=&#39;left&#39;)<br>plt.yticks([])</pre><pre># Reverse the order of the legend<br>handles, labels = ax.get_legend_handles_labels()<br>ax.legend(handles[::-1], labels[::-1], loc=&#39;best&#39;, fontsize=15, bbox_to_anchor=(1.2, 1))</pre><pre>ax.grid(False)</pre><pre>plt.show()</pre><p>I also made an area plot to show the overall emotive qualities of each chapter. This is again a moving average in order to smooth out the more extreme spikes and to show the story arc better across all books:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YtIW8NaCxe1Ltp9_Hl--7Q.png" /></figure><p>The books seem to start with a bit of trailing emotion from the previous story but quickly calm down during the middle chapters only to pick back up again at the end.</p><pre>length = sum([len(hp[book]) for book in hp])<br>window = 10<br>x = np.linspace(0, length - 1, num=length)[int(window / 2): -int(window / 2)]</pre><pre>fig = plt.figure(figsize=(15, 15))<br>ax = fig.add_subplot(1, 1, 1)</pre><pre>y = [movingaverage(hp_df[emotion].tolist(), window)[int(window / 2): -int(window / 2)] for emotion in emotions]</pre><pre>plt.stackplot(x, y, colors=(tab10(0),<br>                            tab10(.1),<br>                            tab10(.2),<br>                            tab10(.3),<br>                            tab10(.4),<br>                            tab10(.5),<br>                            tab10(.6),<br>                            tab10(.7),<br>                            tab10(.8),<br>                            tab10(.9)), labels=emotions)</pre><pre># Plot vertical lines marking the books<br>for book in book_indices:<br>    plt.axvline(x=book_indices[book][0], color=&#39;black&#39;, linewidth=3, linestyle=&#39;:&#39;)<br>plt.axvline(x=book_indices[book][1], color=&#39;black&#39;, linewidth=3, linestyle=&#39;:&#39;)</pre><pre>plt.title(&#39;Emotional Sentiment of the Harry Potter series&#39;, fontsize=20)<br>plt.xticks([(book_indices[book][0] + book_indices[book][1]) / 2 for book in book_indices],<br>           list(hp),<br>           rotation=-30,<br>           fontsize=15,<br>           ha=&#39;left&#39;)<br>plt.yticks([])<br>plt.ylabel(&#39;Relative Sentiment&#39;, fontsize=15)</pre><pre># Reverse the legend<br>handles, labels = ax.get_legend_handles_labels()<br>ax.legend(handles[::-1], labels[::-1], loc=&#39;best&#39;, fontsize=15, bbox_to_anchor=(1.2, 1))</pre><pre>ax.grid(False)</pre><pre>plt.show()</pre><p>Note how in this chart, reversing the legend became necessary for readability. By default, the legend items are added in alphabetical order going down, but the data is stacked from the bottom up. So the colors of the legend and the area plot run in opposite direction — to my eye, quite confusing and difficult to follow. So with ‘Anger’ plotted at the bottom, I also wanted it to be on the bottom of the legend and likewise with ‘Trust’ at the top.</p><p>And lastly, a stacked bar chart to show the weights of the various sentiments across the books:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mjDR4pdbZEDbJiL1Si9l1A.png" /></figure><p>Naturally, words associated with any of the positive emotions would also be associated with the ‘Positive’ sentiment, and likewise for ‘Negative’, so it shouldn’t come as a surprise that those two sentiments carry the bulk of the emotive quality of the books. I find it notable that the emotions are relatively consistent from book to book with just slight differences in magnitude but consistent weights, except for the ‘Fear’ emotion in red; it seems to exhibit the most variance across the series. I also would have expected the cumulative magnitude of sentiments to increase throughout the series as the stakes became higher and higher; however although the final book is indeed the highest, the other 6 books don’t show this gradual increase but almost the opposite, with a constant decline starting with book 2.</p><pre>books = list(hp)<br>margin_bottom = np.zeros(len(books))</pre><pre>fig = plt.figure(figsize=(15, 15))<br>ax = fig.add_subplot(1, 1, 1)</pre><pre>for c, emotion in enumerate(emotions):<br>    y = np.array(hp_df2[emotion])<br>    plt.bar(books, y, bottom=margin_bottom, label=emotion, color=(tab10(c)))<br>    margin_bottom += y</pre><pre># Reverse the legend<br>handles, labels = ax.get_legend_handles_labels()<br>ax.legend(handles[::-1], labels[::-1], loc=&#39;best&#39;, fontsize=15, bbox_to_anchor=(1.2, 1))</pre><pre>plt.title(&#39;Emotional Sentiment of the Harry Potter series&#39;, fontsize=20)<br>plt.xticks(books, books, rotation=-30, ha=&#39;left&#39;, fontsize=15)<br>plt.ylabel(&#39;Relative Sentiment Score&#39;, fontsize=15)<br>plt.yticks([])<br>ax.grid(False)<br>plt.show()</pre><p>The tricky bit in this plot is using the margin_bottom variable to stack each of the columns. Other than that, it just uses a couple tricks from the previous plots.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1b474b13651d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/basic-nlp-on-the-texts-of-harry-potter-sentiment-analysis-1b474b13651d">Basic NLP on the Texts of Harry Potter: Sentiment Analysis</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Text Summarization on the Books of Harry Potter]]></title>
            <link>https://medium.com/data-science/text-summarization-on-the-books-of-harry-potter-5e9f5bf8ca6c?source=rss-3c8205f57821------2</link>
            <guid isPermaLink="false">https://medium.com/p/5e9f5bf8ca6c</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[nlp]]></category>
            <category><![CDATA[data-journalism]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Greg Rafferty]]></dc:creator>
            <pubDate>Wed, 12 Dec 2018 22:23:19 GMT</pubDate>
            <atom:updated>2019-05-22T01:13:20.891Z</atom:updated>
            <content:encoded><![CDATA[<h4>A comparison of several algorithms</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e3Iy0lECtFrPahOX8H-WzA.jpeg" /><figcaption>“Harry sat with Hermione and Ron in the library as the sun set outside, tearing feverishly through page after page of spells, hidden from one another by the massive piles of books on the desk in front.”</figcaption></figure><p>I’m Greg Rafferty, a data scientist in the Bay Area. You can check out the code for this project on my <a href="https://github.com/raffg/harry_potter_nlp">github</a>. Feel free to contact me with any questions!</p><p>In this series of posts, I’m looking at a few handy NLP techniques, through the lens of Harry Potter. Previous posts in this series on basic NLP looked at <a href="https://towardsdatascience.com/basic-nlp-on-the-texts-of-harry-potter-topic-modeling-with-latent-dirichlet-allocation-f3c00f77b0f5">Topic Modeling with Latent Dirichlet Allocation</a> and <a href="https://towardsdatascience.com/regex-on-the-texts-of-harry-potter-96b8a3878303">Regular Expressions</a>, and my next post will look at sentiment analysis.</p><blockquote>Hermione interrupted them. “Aren’t you two ever going to read Hogwarts, A History?”</blockquote><p>How many times throughout the Harry Potter series does Hermione bug Harry and Ron to read the enormous tome <em>Hogwarts, A History</em>? Hint: it’s a lot. How many nights do the three of them spend in the library, reading through every book they can find to figure out who Nicolas Flamel is, or how to survive underwater, or preparing for their O.W.L.s? The mistake they’re all making is to try to read everything themselves.</p><p>Remember when you were in school and stumbled upon the CliffsNotes summary of that book you never read but were supposed to write an essay about? That’s basically what text summarization does: provide the CliffsNotes version for any large document. Now, CliffsNotes have been written by rather well-educated people who are familiar with the book they’re summarizing. But this is the twenty-first century, aren’t computers supposed to be putting humans out of work? I looked into a few text summarization algorithms to see if we’re ready to put poor old <a href="https://en.wikipedia.org/wiki/Clifton_Hillegass">Clifton</a> out of a job.</p><p>There are two types of text summarization algorithms: extractive and abstractive. All extractive summarization algorithms attempt to score the phrases or sentences in a document and return only the most highly informative blocks of text. Abstractive text summarization actually creates new text which doesn’t exist in that form in the document. Abstractive summarization is what you might do when explaining a book you read to your friend, and it is much more difficult for a computer to do than extractive summarization. Computers just aren’t that great at the act of creation. To date, there aren’t any abstractive summarization techniques which work suitably well on long documents. The best performing ones merely create a sentence based upon a single paragraph, or cut the length of a sentence in half while maintaining as much information as possible. Often, grammar suffers horribly. They’re usually based upon neural network models. This post will focus on the much more simple extractive text summarization techniques.</p><p>Most of the algorithms I’ll present are packaged together in the sumy package for Python but I also use one summarizer from the Gensim package and one other technique I wrote myself using LDA topic keywords to enrich the sumy EdmundsonSummarizer. All examples output five-sentence summaries of the first chapter of <em>Harry Potter and the Sorcerer’s Stone</em>. See my <a href="https://github.com/raffg/harry_potter_nlp/blob/master/text_summarization.ipynb">Jupyter notebook</a> for complete code. And a word of caution: don’t judge the results too harshly. They’re not great… (text summarization does seem to work better on drier works of non-fiction)</p><h4>LexRank Summarizer</h4><p><a href="https://www.cs.cmu.edu/afs/cs/project/jair/pub/volume22/erkan04a-html/erkan04a.html">LexRank</a> is an unsupervised approach that gets its inspiration from the same ideas behind Google’s PageRank algorithm. The authors say it is “based on the concept of eigenvector centrality in a graph representation of sentences”, using “a connectivity matrix based on on intra-sentence cosine similarity.” Ok, so in a nutshell, it finds the relative importance of all words in a document and selects the sentences which contain the most of those high-scoring words.</p><pre>&quot;The Potters, that&#39;s right, that&#39;s what I heard —&quot; &quot;— yes, their son, Harry —&quot; Mr. Dursley stopped dead.<br>Twelve times he clicked the Put-Outer, until the only lights left on the whole street were two tiny pinpricks in the distance, which were the eyes of the cat watching him.<br>Dumbledore slipped the Put-Outer back inside his cloak and set off down the street toward number four, where he sat down on the wall next to the cat.<br>&quot;But I c-c-can&#39;t stand it — Lily an&#39; James dead — an&#39; poor little Harry off ter live with Muggles —&quot; &quot;Yes, yes, it&#39;s all very sad, but get a grip on yourself, Hagrid, or we&#39;ll be found,&quot; Professor McGonagall whispered, patting Hagrid gingerly on the arm as Dumbledore stepped over the low garden wall and walked to the front door.<br>Dumbledore turned and walked back down the street.</pre><h4>Luhn Summarizer</h4><p>One of the first text summarization algorithms was published in 1958 by <a href="http://altaplana.com/ibm-luhn58-BusinessIntelligence.pdf">Hans Peter Luhn</a>, working at IBM research. Luhn’s algorithm is a naive approach based on TF-IDF and looking at the “window size” of non-important words between words of high importance. It also assigns higher weights to sentences occurring near the beginning of a document.</p><pre>It was now reading the sign that said Privet Drive — no, looking at the sign; cats couldn&#39;t read maps or signs.<br>He didn&#39;t see the owls swooping past in broad daylight, though people down in the street did; they pointed and gazed open-mouthed as owl after owl sped overhead.<br>No one knows why, or how, but they&#39;re saying that when he couldn&#39;t kill Harry Potter, Voldemort&#39;s power somehow broke — and that&#39;s why he&#39;s gone.&quot;<br>&quot;But I c-c-can&#39;t stand it — Lily an&#39; James dead — an&#39; poor little Harry off ter live with Muggles —&quot; &quot;Yes, yes, it&#39;s all very sad, but get a grip on yourself, Hagrid, or we&#39;ll be found,&quot; Professor McGonagall whispered, patting Hagrid gingerly on the arm as Dumbledore stepped over the low garden wall and walked to the front door.<br>G&#39;night, Professor McGonagall — Professor Dumbledore, sir.&quot;</pre><h4>LSA Summarizer</h4><p><a href="http://lsa.colorado.edu/papers/JASIS.lsi.90.pdf">Latent Semantic Analysis</a> is a relatively new algorithm which combines term frequency with singular value decomposition.</p><pre>He dashed back across the road, hurried up to his office, snapped at his secretary not to disturb him, seized his telephone, and had almost finished dialing his home number when he changed his mind.<br>It seemed that Professor McGonagall had reached the point she was most anxious to discuss, the real reason she had been waiting on a cold, hard wall all day, for neither as a cat nor as a woman had she fixed Dumbledore with such a piercing stare as she did now.<br>He looked simply too big to be allowed, and so wild — long tangles of bushy black hair and beard hid most of his face, he had hands the size of trash can lids, and his feet in their leather boots were like baby dolphins.<br>For a full minute the three of them stood and looked at the little bundle; Hagrid&#39;s shoulders shook, Professor McGonagall blinked furiously, and the twinkling light that usually shone from Dumbledore&#39;s eyes seemed to have gone out.<br>A breeze ruffled the neat hedges of Privet Drive, which lay silent and tidy under the inky sky, the very last place you would expect astonishing things to happen.</pre><h4>TextRank Summarizer</h4><p><a href="https://web.eecs.umich.edu/~mihalcea/papers/mihalcea.emnlp04.pdf">TextRank</a> is another text summarizer based on the ideas of PageRank, and was also developed at the same time as LexRank, though by different groups of people. TextRank is a bit more simplistic than LexRank; although both algorithms are very similar, LexRank applies a heuristic post-processing step to remove sentences with highly duplicitous.</p><pre>Mr. and Mrs. Dursley, of number four, Privet Drive, were proud to say that they were perfectly normal, thank you very much.<br>They were the last people you&#39;d expect to be involved in anything strange or mysterious, because they just didn&#39;t hold with such nonsense.<br>Mr. Dursley was the director of a firm called Grunnings, which made drills.<br>He was a big, beefy man with hardly any neck, although he did have a very large mustache.<br>Mrs. Dursley was thin and blonde and had nearly twice the usual amount of neck, which came in very useful as she spent so much of her time craning over garden fences, spying on the neighbors.</pre><h4>Edmundson Summarizer</h4><p>In 1969, Harold Edmundson <a href="http://courses.ischool.berkeley.edu/i256/f06/papers/edmonson69.pdf">developed the summarizer</a> bearing his name. Edmundson’s algorithm was, along with Luhn’s, one of the seminal text summarization techniques. What sets the Edmundson summarizer apart from the others is that it takes into account “bonus words”, words stated by the user as of high importance; “stigma words”, words of low importance or even negative importance; and “stop words”, which are the same as used elsewhere in NLP processing. Edmundson suggested using the words in a document’s title as bonus words. Using the chapter title as bonus words, this is what Edmundson outputs:</p><pre>The Dursleys shuddered to think what the neighbors would say if the Potters arrived in the street.<br>When Dudley had been put to bed, he went into the living room in time to catch the last report on the evening news: &quot;And finally, bird-watchers everywhere have reported that the nation&#39;s owls have been behaving very unusually today.<br>Twelve times he clicked the Put-Outer, until the only lights left on the whole street were two tiny pinpricks in the distance, which were the eyes of the cat watching him.<br>Dumbledore slipped the Put-Outer back inside his cloak and set off down the street toward number four, where he sat down on the wall next to the cat.<br>He couldn&#39;t know that at this very moment, people meeting in secret all over the country were holding up their glasses and saying in hushed voices: &quot;To Harry Potter — the boy who lived!&quot;</pre><p>Another addition I made was to use <a href="https://towardsdatascience.com/basic-nlp-on-the-texts-of-harry-potter-topic-modeling-with-latent-dirichlet-allocation-f3c00f77b0f5">LDA</a> to extract topic keywords, and then add those topic keywords back in as additional bonus words. With this modification, the Edmundson results are as follows:</p><pre>At half past eight, Mr. Dursley picked up his briefcase, pecked Mrs. Dursley on the cheek, and tried to kiss Dudley good-bye but missed, because Dudley was now having a tantrum and throwing his cereal at the walls.<br>When Dudley had been put to bed, he went into the living room in time to catch the last report on the evening news: &quot;And finally, bird-watchers everywhere have reported that the nation&#39;s owls have been behaving very unusually today.<br>Twelve times he clicked the Put-Outer, until the only lights left on the whole street were two tiny pinpricks in the distance, which were the eyes of the cat watching him.<br>One small hand closed on the letter beside him and he slept on, not knowing he was special, not knowing he was famous, not knowing he would be woken in a few hours&#39; time by Mrs. Dursley&#39;s scream as she opened the front door to put out the milk bottles, nor that he would spend the next few weeks being prodded and pinched by his cousin Dudley.<br>He couldn&#39;t know that at this very moment, people meeting in secret all over the country were holding up their glasses and saying in hushed voices: &quot;To Harry Potter — the boy who lived!&quot;</pre><h4>SumBasic Summarizer</h4><p>The <a href="https://www.cis.upenn.edu/~nenkova/papers/ipm.pdf">SumBasic</a> algorithm was developed in 2005 and uses only the word probability approach to determine sentence importance. Sorry, but it’s pretty bad on this document.</p><pre>Mr. Dursley wondered.<br>&quot;Harry.<br>The cat was still there.<br>&quot;It certainly seems so,&quot; said Dumbledore.<br>&quot;Yes,&quot; said Professor McGonagall.</pre><p>Wow. That’s terrible. Ahem, moving on…</p><h4>KL Summarizer</h4><p>The <a href="http://www.aclweb.org/anthology/N09-1041">KLSum</a> algorithm is a greedy method which adds sentences to the summary as long as the <a href="https://en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence">KL Divergence</a> (a measure of entropy) is decreasing.</p><pre>It was on the corner of the street that he noticed the first sign of something peculiar — a cat reading a map.<br>It grew steadily louder as they looked up and down the street for some sign of a headlight; it swelled to a roar as they both looked up at the sky — and a huge motorcycle fell out of the air and landed on the road in front of them.<br>He looked simply too big to be allowed, and so wild — long tangles of bushy black hair and beard hid most of his face, he had hands the size of trash can lids, and his feet in their leather boots were like baby dolphins.<br>&quot;But I c-c-can&#39;t stand it — Lily an&#39; James dead — an&#39; poor little Harry off ter live with Muggles —&quot; &quot;Yes, yes, it&#39;s all very sad, but get a grip on yourself, Hagrid, or we&#39;ll be found,&quot; Professor McGonagall whispered, patting Hagrid gingerly on the arm as Dumbledore stepped over the low garden wall and walked to the front door.<br>He clicked it once, and twelve balls of light sped back to their street lamps so that Privet Drive glowed suddenly orange and he could make out a tabby cat slinking around the corner at the other end of the street.</pre><h4>Reduction Summarizer</h4><p>The Reduction algorithm is another graph-based model which values sentences according to the sum of the weights of their edges to other sentences in the document. This weight is computed in the same way as it is in the TexRank model.</p><pre>Mrs. Potter was Mrs. Dursley&#39;s sister, but they hadn&#39;t met for several years; in fact, Mrs. Dursley pretended she didn&#39;t have a sister, because her sister and her good-for-nothing husband were as unDursleyish as it was possible to be.<br>It seemed that Professor McGonagall had reached the point she was most anxious to discuss, the real reason she had been waiting on a cold, hard wall all day, for neither as a cat nor as a woman had she fixed Dumbledore with such a piercing stare as she did now.<br>Dumbledore took Harry in his arms and turned toward the Dursleys&#39; house.<br>&quot;But I c-c-can&#39;t stand it — Lily an&#39; James dead — an&#39; poor little Harry off ter live with Muggles —&quot; &quot;Yes, yes, it&#39;s all very sad, but get a grip on yourself, Hagrid, or we&#39;ll be found,&quot; Professor McGonagall whispered, patting Hagrid gingerly on the arm as Dumbledore stepped over the low garden wall and walked to the front door.<br>G&#39;night, Professor McGonagall — Professor Dumbledore, sir.&quot;</pre><h4>Gensim Summarizer</h4><p>The <a href="https://radimrehurek.com/gensim/summarization/summariser.html">Gensim package for Python includes a summarizer</a> which is a <a href="https://arxiv.org/abs/1602.03606">modification of the TextRank algorithm</a>. Gensim’s approach modifies the sentence similarity function.</p><pre>It was now reading the sign that said Privet Drive — no, looking at the sign; cats couldn&#39;t read maps or signs.<br>Dumbledore slipped the Put-Outer back inside his cloak and set off down the street toward number four, where he sat down on the wall next to the cat.<br>They&#39;re a kind of Muggle sweet I&#39;m rather fond of.&quot; &quot;No, thank you,&quot; said Professor McGonagall coldly, as though she didn&#39;t think this was the moment for lemon drops.<br>All this &#39;You-Know-Who&#39; nonsense — for eleven years I have been trying to persuade people to call him by his proper name: Voldemort.&quot; Professor McGonagall flinched, but Dumbledore, who was unsticking two lemon drops, seemed not to notice.<br>Professor McGonagall shot a sharp look at Dumbledore and said, &quot;The owls are nothing next to the rumors that are flying around.<br>It seemed that Professor McGonagall had reached the point she was most anxious to discuss, the real reason she had been waiting on a cold, hard wall all day, for neither as a cat nor as a woman had she fixed Dumbledore with such a piercing stare as she did now.</pre><p>So which algorithm do you think provides the best summary? There are a lot of parameters to tweak before making a final judgement. Some might perform better on shorter summaries, some on longer summaries. The style of writing could make a difference (I mentioned before that I’ve had better luck summarizing non-fiction than fiction). But if you’re Harry just entering Hogwarts’ library with the intent to answer some question and are intimidated by “the sheer size of the library; tens of thousands of books; thousands of shelves; hundreds of narrow rows”, then it couldn’t hurt to have a couple summaries available with a few mouse clicks. If only wizards used such silly muggle creations…</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5e9f5bf8ca6c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/text-summarization-on-the-books-of-harry-potter-5e9f5bf8ca6c">Text Summarization on the Books of Harry Potter</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Regex on the Texts of Harry Potter]]></title>
            <link>https://medium.com/data-science/regex-on-the-texts-of-harry-potter-96b8a3878303?source=rss-3c8205f57821------2</link>
            <guid isPermaLink="false">https://medium.com/p/96b8a3878303</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[towards-data-science]]></category>
            <category><![CDATA[nlp]]></category>
            <dc:creator><![CDATA[Greg Rafferty]]></dc:creator>
            <pubDate>Sun, 09 Dec 2018 23:55:42 GMT</pubDate>
            <atom:updated>2019-05-22T01:13:32.969Z</atom:updated>
            <content:encoded><![CDATA[<h4>A deep-dive case study</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/722/1*6HgHwD5CFg3eo4XoEMPJkw.png" /><figcaption>Regular Expressions: more mind-boggling than arithmancy</figcaption></figure><p>I’m Greg Rafferty, a data scientist in the Bay Area. You can check out the code for this project on my <a href="https://github.com/raffg/harry_potter_nlp">github</a>. Feel free to contact me with any questions!</p><p>In this series of posts, I’m looking at a few handy NLP techniques, through the lens of Harry Potter. My previous post in this series on basic NLP looked at <a href="https://towardsdatascience.com/basic-nlp-on-the-texts-of-harry-potter-topic-modeling-with-latent-dirichlet-allocation-f3c00f77b0f5">Topic Modeling with Latent Dirichlet Allocation</a>, and my next post will look at text summarization.</p><p>Throughout this NLP project, I’ve been using as my text corpus the collection of seven books about Harry Potter. But before I could send these books through my algorithms, I first had to save the pdfs as txt files and then extract the chapters as separate documents. To do this, I used regular expressions. For a good intro to regex in Python, check out <a href="https://developers.google.com/edu/python/regular-expressions">Google’s quick course</a>.</p><p>Regular expressions can be thought of as wildcard search on crack. They allow an incredible amount of flexibility to describe patterns in text strings. If you’ve never seen them before, a regular expression pattern can look like a jumbled, senseless mess. But there is order in that chaos, and a great deal of power. Let’s explore this with the pattern I used on these texts:</p><pre>pattern = r&quot;((?:[A-Z-][ ]){9,}[A-Z])\s+([A-Z \n&#39;,.-]+)\b(?![A-Z]+(?=\.))\b(?![a-z&#39;]|[A-Z.])(.*?)(?=(?:[A-Z][ ]){9,}|This book)&quot;</pre><p>When applied with re.findall to a Harry Potter book saved as a text file, the output is a list of 3-element tuples. Each tuple in the list corresponds to a chapter in the book and takes the form (‘C H A P T E R O N E’, ‘THE BOY WHO LIVED’, ‘Mr. and Mrs. Dursley, of number four, Privet Drive, blah, blah, blah...’) and contains the text of the entire chapter. We need to extract that from this:</p><blockquote>C H A P T E R O N E</blockquote><blockquote>THE BOY WHO LIVED</blockquote><blockquote>Mr. and Mrs. Dursley, of number four, Privet Drive, were proud to say that they were perfectly normal, thank you very much. They were the last people you&#39;d expect to be involved in anything strange or mysterious, because they just didn&#39;t hold with such...</blockquote><p>Let’s walk through that pattern above and see how it does this. I’ll describe the logic behind this expression but for more details about the precise syntax, follow along <a href="https://regex101.com/r/orgYwY/8">here</a> for a character-by-character description. This railroad diagram displays the pipeline:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0hKtG73XiAedkH6DuwGheA.png" /><figcaption><a href="https://regexper.com/#%28%28%3F%3A%5BA-Z-%5D%5B%20%5D%29%7B9%2C%7D%5BA-Z%5D%29%5Cs%2B%28%5BA-Z%20%5Cn&#39;%2C.-%5D%2B%29%5Cb%28%3F!%5BA-Z%5D%2B%28%3F%3D%5C.%29%29%5Cb%28%3F!%5Ba-z&#39;%5D%7C%5BA-Z.%5D%29%28.*%3F%29%28%3F%3D%28%3F%3A%5BA-Z%5D%5B%20%5D%29%7B9%2C%7D%7CThis%20book%20%5Cn%29">Source</a></figcaption></figure><p>Group #1 is this part: ((?:[A-Z-][ ]){9,}[A-Z]). That’s the first box on the left in the diagram above. The outer parenthesis tell python to capture what’s inside as Group 1 (this will end up being &#39;C H A P T E R O N E&#39;). The inner parenthesis are a grouping of a sub-pattern and the ?: instructs python not to capture it as a second group. The sub-pattern, ([A-Z][ ]){9,}[A-Z], means any UPPERCASE letter followed by a space, repeated 9 or more times, and ending with a final UPPERCASE letter. This is how the chapter numbers are captured.</p><p>It is followed by \s+ which is not in parenthesis, so not captured, and instructs python to look for white space (line breaks, tabs, spaces, etc), one or more times. This essentially skips down to the chapter title, captured in Group #2, the next set of parenthesis: ([A-Z]\n&#39;,.-]+). This again means any UPPERCASE letter, and/or line break (\n), apostrophe, comma, period, and dash, one or more times. Some of the chapter titles have line breaks or the other various punctuation marks, for instance:</p><blockquote>Prisoner of Azkaban, Chapter 18: MOONEY, WORMTAIL, PADFOOT, AND PRONGS</blockquote><blockquote>Goblet of Fire, Chapter 21: THE HOUSE-ELF LIBERATION FRONT</blockquote><blockquote>Order of the Phoenix, Chapter 8: THE WOES OF MRS. WEASLEY</blockquote><p>However, there’s one problem with this: The fourth chapter of book 1 starts with</p><blockquote>BOOM. They knocked again…</blockquote><p>So by looking for all UPPERCASE letters, that “BOOM.” would be captured as part of the title. We need to use what’s called a negative lookahead, declared with ?!: /b(?![A-Z]+(?=\.))/b. The \b signs mean word boundaries. So the chapter title capturing string above captures all UPPERCASE letters <em>unless</em> the next word is also all UPPERCASE but immediately followed by a period, declared with a positive lookahead ?=. Great! Now we’re able to find that “BOOM.” and exclude it from our capture. But this introduces another problem. One of the chapters I mentioned in the previous paragraph, “THE WOES OF MRS. WEASLEY”, also contains a word followed by a period so now we’d only capture the chapter title as “THE WOES OF”. We need <em>another</em> negative lookahead, (?![a-z&#39;]|[A-Z.]), to make sure that all UPPERCASE words followed by a period are not <em>also</em> followed by lowercase words (chapter text), or are not the last word in the UPPERCASE string (because although the chapter title may contain a period, it never ends in one).</p><p>Group #3 is the easiest one: (.*?). This means capture any character, any number of times, and be greedy about it. Keep going until forced to stop, with the next part.</p><p>The last part of the regular expression tells python at what point to stop capturing text: (?=(?:[A-Z][ ]){9,}|This book \n). This is another non-captured lookahead. It provides instructions to stop capturing text once a sequence of UPPERCASE letter and space is repeated at least 9 times (because thus begins the next chapter, &quot;C H A P T E R T W O”), or until the string This book \n. This final string marks the end of the book; for each of the seven Harry Potter books ends with this text:</p><blockquote>This book <br>was art directed by <br>David Saylor and designed by Becky <br>Terhune. The art for both the jacket and interior was <br>created using pastels on toned printmaking paper. The text was <br>set in 12-point Adobe Garamond, a typeface based on the sixteenth- <br>century type designs of Claude Garamond redrawn by Robert <br>Slimbach in 1989. The book was printed and bound <br>at RR Donnelley &amp; Sons, Willard, Oh. <br>The production was supervised by <br>Angela Biola</blockquote><p>Phew! I hope that all made sense! I’d highly encourage you to visit <a href="https://regex101.com/r/orgYwY/8">this link</a> to see this all in action with even more detail and a character-by-character description of what everything is doing.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=96b8a3878303" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/regex-on-the-texts-of-harry-potter-96b8a3878303">Regex on the Texts of Harry Potter</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[LDA on the Texts of Harry Potter]]></title>
            <link>https://medium.com/data-science/basic-nlp-on-the-texts-of-harry-potter-topic-modeling-with-latent-dirichlet-allocation-f3c00f77b0f5?source=rss-3c8205f57821------2</link>
            <guid isPermaLink="false">https://medium.com/p/f3c00f77b0f5</guid>
            <category><![CDATA[nlp]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[data-journalism]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Greg Rafferty]]></dc:creator>
            <pubDate>Fri, 07 Dec 2018 01:12:44 GMT</pubDate>
            <atom:updated>2019-05-22T01:13:43.796Z</atom:updated>
            <content:encoded><![CDATA[<h4>Topic Modeling with Latent Dirichlet Allocation</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jBR-iVeF4fraWJt3Uyi7cg.jpeg" /><figcaption>“Hmm,” said a small voice in his ear. “Difficult. Very difficult. Plenty of courage, I see. Not a bad mind either. There’s talent, oh my goodness, yes — and a nice thirst to prove yourself, now that’s interesting. . . . So where shall I put you?”</figcaption></figure><p>I’m Greg Rafferty, a data scientist in the Bay Area. You can check out the code for this project on my <a href="https://github.com/raffg/harry_potter_nlp">github</a>. Feel free to contact me with any questions!</p><p>In this post, I’ll describe topic modeling with Latent Dirichlet Allocation and compare different algorithms for it, through the lens of Harry Potter. Upcoming posts will cover two more NLP tasks: text summarization and sentiment analysis.</p><p>Recently, I was put on a new project with a team who were unanimously shocked and disappointed that I’d never read nor seen the movies about a certain fictional wizard named Harry Potter. In order to fit in with the team, and obviously save my career from an early end, it quickly became evident that I was going to have to take a crash-course in the happenings at Hogwarts. Armed with my ebook reader and seven shiny new pdf files, I settled in to see what the fuss was all about. Meanwhile, I had started working on a side project composed of a bunch of unrelated NLP tasks. I needed a good sized set of text documents and I thought all of these shiny new pdfs would be a great source.</p><p>And somewhere around the middle of the third book, I suddenly realized that LDA was basically just an algorithmic Sorting Hat.</p><p>LDA, or Latent Dirichlet Allocation, is a generative probabilistic model of (in NLP terms) a corpus of documents made up of words and/or phrases. The model consists of two tables; the first table is the probability of selecting a particular word in the corpus when sampling from a particular topic, and the second table is the probability of selecting a particular topic when sampling from a particular document.</p><p>Here’s an example. Let’s say I’ve got these three (rather non-sensical) documents:</p><pre>document_0 = &quot;Harry Harry wand Harry magic wand&quot;<br>document_1 = &quot;Hermione robe Hermione robe magic Hermione&quot;<br>document_2 = &quot;Malfoy spell Malfoy magic spell Malfoy&quot;<br>document_3 = &quot;Harry Harry Hermione Hermione Malfoy Malfoy&quot;</pre><p>Here’s the term-frequency matrix for these documents:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FqAg3LORQBABVBjVrz_EOw.png" /></figure><p>Just from glancing at this, it seems pretty obvious that document 0 is mostly about Harry, a little bit about magic, and partly about wand. Document 1 is also a little bit about magic, but mostly about Hermione and robe. And document 2 is again partly about magic, but mostly about Malfoy and spell. Document 3 is equally about Harry, Hermione, and Malfoy. It’s usually not so easy to see this because a more practical corpus would consists of thousands or ten-of-thousands of words, so let’s see what the LDA algorithm chooses for topics:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/879/1*aKHZSmNebGvIAW1lZJr2rQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/879/1*b-pDFliu4rSgJny56JJPSQ.png" /><figcaption>Data Science with Excel!</figcaption></figure><p>And that’s roughly what we predicted just by going with term frequencies and our gut. The number of topics is a hyperparameter you’ll need to choose and tune carefully, and I’ll go into that later, but for this example I chose 4 topics to make my point. The upper table shows words versus topics and the lower table shows documents versus topics. Each column in the upper table and each row in the lower table must sum to 1. These tables are telling us that if we were to randomly sample a word from Topic 0, there’s a 70.9% chance we’d grab “Harry”. If we chose a word from Topic 3, it’s near certain that we’d pick “magic”. If we sampled Document 3, there’s an equal chance that we would pick Topic 0, 1, or 2.</p><p>It’s up to us as smart humans who can infer meaning from a bag of words to name these topics. In these examples with a <em>very</em> limited vocabulary, the topics quite obviously correspond to single words. If we had run LDA on, say, a couple thousand restaurant descriptions, we might find topics corresponding to cuisine type or atmosphere. It’s important to note that LDA, unlike typical clustering algorithms such as Kmeans, allows a document to exist in multiple topics. So in those restaurant descriptions, we might find one restaurant placed in the “Italian”, “date-night”, and “cafe” topics.</p><p>So how is all of this like the Sorting Hat? All new students at Hogwarts go through a ceremony when they arrive on day one to determine which house they’ll be in (I’m probably the only person who didn’t know this up until a few weeks ago). The Sorting Hat, once placed on someone’s head, understands what is in their thoughts, dreams, and experiences. This is a bit like LDA building the term-frequency matrix and understanding what words and N-grams are contained within each document.</p><p>The Sorting Hat then compares the student’s attributes with the attributes of the various houses (bravery goes to Gryffindor, loyalty to Hufflepuff, wisdom to Ravenclaw, and sneaky, shifty sleazeballs go to Slytherin (ok, just a quick aside — can <em>ANYONE</em> explain to me why Slytherin has persisted for the thousand-year history of this school? It’s like that one fraternity which finds itself in yet another ridiculously obscene scandal every damn year!)). This is where LDA creates the word-topic table and begins to associate the attributes of the topics.</p><p>Harry’s placement was notably split between Gryffindor and Slytherin due to his combination of courage, intelligence, talent, and ambition, but Gryffindor just slightly edged out ahead and Harry Potter became the hero of an entire generation of young millennials instead of its villain. This is where LDA creates the document-topic table and finally determines which is the dominant topic for each document.</p><p>OK, so now that we know roughly what LDA does, let’s look at two different implementations in Python. Check out my <a href="https://github.com/raffg/basic_nlp">Github repo</a> for all of the nitty-gritty details.</p><p>First of all, one of the best ways to determine how many topics you should model is with an elbow plot. This is the same technique often used to determine how many clusters to choose with the clustering algorithms. In this case, we’ll plot the coherence score against the number of topics:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/619/1*YAM49i0Jj4lfqj1YOhxeQw.png" /></figure><p>You’ll generally want to pick the lowest number of topics where the coherence score begins to level off. This is why it’s called an elbow plot — you pick the elbow between steep gains and shallow gains. In this case (and it’s a remarkably spiky case; usually the curves are a little bit smoother than this), I’d go with somewhere around 20 topics.</p><p>The first model I used is Gensim’s <a href="https://radimrehurek.com/gensim/models/ldamodel.html">ldamodel</a>. At 20 topics, Gensim had a coherence score of 0.319. This is not great; indeed the Mallet algorithm which we’ll look at next almost always outperforms Gensim’s. However, one really cool thing with Gensim is the pyLDAvis, an interactive chart you can run in a Jupyter notebook. It plots the clusters with two principal components and shows the proportion of words in each cluster:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YwShiwMV2KGKMFOQOBrERg.png" /><figcaption>Harry Potter and the Allocation of Dirichlet</figcaption></figure><p>The next implementation I looked at was Mallet (<strong>MA</strong>chine <strong>L</strong>earning for <strong>L</strong>anguag<strong>E</strong> <strong>T</strong>oolkit), a Java-based package put out by UMASS Amherst. The difference between Mallet and Gensim’s standard LDA is that Gensim uses a Variational Bayes sampling method which is faster but less precise that Mallet’s Gibbs Sampling. Fortunately for those who prefer to code in Python, Gensim has a wrapper for Mallet: <a href="https://radimrehurek.com/gensim/models/ldamallet.html">Latent Dirichlet Allocation via Mallet</a>. In order to use it, you need to download the Mallet Java package from here <a href="http://mallet.cs.umass.edu/dist/mallet-2.0.8.zip">http://mallet.cs.umass.edu/dist/mallet-2.0.8.zip</a> and also install the Java Development Kit. Once everything is set up, implementing the model is pretty much the same as Gensim’s standard model. Using Mallet, the coherence score for the 20-topic model increased to 0.375 (remember, Gensim’s standard model output 0.319). It’s a modest increase, but usually persists with a variety of data sources so although Mallet is slightly slower, I prefer it for its increase in return.</p><p>Finally, I built a Mallet model on the 192 chapters of all 7 books in the Harry Potter series. Here are the top 10 keywords the model output for each latent topic. How would you name these clusters?</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e8171bb4c7b021db298ef7b732ec0beb/href">https://medium.com/media/e8171bb4c7b021db298ef7b732ec0beb/href</a></iframe><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f3c00f77b0f5" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/basic-nlp-on-the-texts-of-harry-potter-topic-modeling-with-latent-dirichlet-allocation-f3c00f77b0f5">LDA on the Texts of Harry Potter</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>