<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://stphnma.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://stphnma.github.io/" rel="alternate" type="text/html" /><updated>2026-07-20T05:27:58+00:00</updated><id>https://stphnma.github.io/feed.xml</id><title type="html">Stephen Ma</title><subtitle>Research engineer building machine-learning systems, models, and tools.</subtitle><entry><title type="html">Building an election model in pymc3</title><link href="https://stphnma.github.io/writing/election-modelling/" rel="alternate" type="text/html" title="Building an election model in pymc3" /><published>2020-12-29T00:00:00+00:00</published><updated>2020-12-29T00:00:00+00:00</updated><id>https://stphnma.github.io/writing/election-modelling</id><content type="html" xml:base="https://stphnma.github.io/writing/election-modelling/"><![CDATA[<script defer="" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
</script>

<h1 id="anyone-can-make-an-election-model">Anyone can make an election model</h1>

<blockquote>
  <p>Building the Economist’s election forecast model in pymc3</p>
</blockquote>

<p>I’ve never been very interested in politics. But like many others, I got caught up in the hubbub of this past election, especially given the high stakes involved. So I started paying more attention to forecasts from FiveThirtyEight and TheEconomist, followed Nate Silver on Twitter, and even got distracted by the popcorn drama <a href="https://nymag.com/intelligencer/2020/10/nate-silver-and-g-elliott-morris-are-fighting-on-twitter.html">flame war</a> between the two election forecasters.</p>

<p><img src="https://media.giphy.com/media/12aW6JtfvUdcdO/giphy.gif" alt="Alt Text" /></p>

<p>As well as reading posts by Andrew Gelman – for anyone curious, he has some fantastic posts digging into the components of the Economist model.</p>

<p>All this also got me curious as to how hard it would be to build one of their forecast models.
<img src="https://stphnma.github.io/assets/is-this-machine-learning.png" alt="State Trace" /></p>

<p>As it turns out, it’s not too difficult. I can’t speak about FiveThirtyEight’s model, but the folks at the Economist were kind of enough to put their model and datasets on Github, so anyone can go through their code and reverse-engineer it. Their model is built in R and Stan – I have no familiarity in Stan, but I thought it would be a fun project to try to rebuild their model in Python with <code class="language-plaintext highlighter-rouge">pymc3</code>.</p>

<h2 id="how-the-model-works">How the model works</h2>

<p>If you just take a look at <a href="https://github.com/TheEconomist/us-potus-model">their repo</a>, they state the inspiration for the model</p>
<blockquote>
  <p>“Improving on Pierre Kremp’s implementation of Drew Linzer’s dynamic linear model for election forecasting”</p>
</blockquote>

<p>And if you follow the links, you get directed to <a href="https://votamatic.org/wp-content/uploads/2013/07/Linzer-JASA13.pdf">this paper</a>, which seems to be the source model. (By the way, I encourage anyone interested to read the actual paper, the math in it is pretty accessible and it’s a nice application of Bayesian modeling.)</p>

<p>This is the model we’re going to build in <code class="language-plaintext highlighter-rouge">pymc3</code>. It’s not exactly the Economist’s model, but it’s close enough – their model is a bit more complicated by trying to account include between state covariances and polling biases. I’m not a real election forecaster, so we’re going to keep it simple.</p>

<p>At it’s core, the model has two main components:</p>
<ol>
  <li>A “structural” forecast, based on economic and political factors</li>
  <li>A series of random walks describing the trends of voter preferences throughout time.</li>
</ol>

<p>We’ll break these down one by one:</p>

<h3 id="1-structural-forecast">1. Structural Forecast</h3>
<p>The structural model turns out based on an earlier model known as <em>Time for a Change</em>, described in <a href="https://www.washingtonpost.com/blogs/ezra-klein/files/2012/08/abramowitz.pdf">this paper</a>. This model predicts the current presidential incumbent’s vote percentage from a regression on a few features, including the incumbent’s June approval rating and the latest Q2 GDP growth numbers. I don’t have much background on this, but supposedly this model worked well in the past.</p>

<p>There’s nothing Bayesian about this model – instead we’ll be using it’s prediction as a prior for the larger model</p>

<h3 id="2-voter-preference-trends">2. Voter Preference Trends</h3>
<p>For this part, we assume that trends in state-level preferences are governed by two random walks: a national component \(\alpha\), and a state-specific component  \(\beta_{s}\). Combining those, we end up with a stochastic variable representing state level preferences at time t.</p>
<div>
$$\pi_{s,\ t} = logit(\beta_{s,\ t} + \alpha_{t})$$
</div>

<p>(Logit is used to restrict the variable between 0 and 1)</p>

<p>The trend for \(\pi_{s,\ t}\) determines results in the polling data, given by the relationship</p>
<div>
$$
y_{s,\ t} \sim Binomial(\pi_{s,\ t})
$$
where y is the number of poll results favoring a specific candidates.
</div>
<p>The \(\beta\) and \(\alpha\) parameters are then modeled as a reverse random walk (I didn’t know you can do this!), given by:</p>

<div>
$$
\beta_{s,\ t} \sim Normal(\beta_{s,\ t+1})
\\
\alpha_{t} \sim Normal(\alpha_{t+1}, \sigma_{\alpha})
$$
</div>
<p>The structural forecast is used to set the time T (election date) prior on the state-level preferences \(\beta_{s,\ t}\)  If you think about it, this makes sense, since the structural forecast is trying to predict what the preferences are going to be on the election date, not 90 or so days before.</p>

<p>\(\alpha\) is given a prior at time T of 0 – the reason being that on election day, we expect votes to be determined purely by state-specific effects. If you look at the paper, they describe it as “a national-level effect detects systematic departures from Beta”.</p>

<div>
$$
\beta_{s,\ T} \sim Normal(h_{s}, \sigma_{s})
\\
\alpha_{T} \sim Normal(0, \sigma_{\alpha})
$$
</div>

<p>Even though this is now a reverse random walk, I think intuitively we can still think of this as a normal random walk.  The key thing is that the prior is just on the last time step instead of the first. You can think this as saying, absent any data, we expect voter preferences on election day (day T) to be what we forecasted from the fundamentals model.</p>

<h2 id="show-me-the-code">Show me the code</h2>

<p>Let’s apply this model on data from the 2012 election. Here we’re just fitting a simple linear regression model
  Incumbent vote ~ June Approval Rates + Q2 GDP Growth</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">from</span> <span class="nn">sklearn.linear_model</span> <span class="kn">import</span> <span class="n">LinearRegression</span>

<span class="k">def</span> <span class="nf">fit_fundamentals_model</span><span class="p">():</span>
    <span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">'data/abramowitz_data.csv'</span><span class="p">)</span>
    <span class="n">train_df</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="n">df</span><span class="p">[</span><span class="s">'year'</span><span class="p">]</span> <span class="o">&lt;</span> <span class="mi">2012</span><span class="p">]</span>
    <span class="n">model</span> <span class="o">=</span> <span class="n">LinearRegression</span><span class="p">(</span><span class="n">fit_intercept</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">X</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">train_df</span><span class="p">[[</span><span class="s">'juneapp'</span><span class="p">,</span> <span class="s">'q2gdp'</span><span class="p">]],</span> <span class="n">df</span><span class="p">[</span><span class="s">'incvote'</span><span class="p">]</span>
    <span class="n">model</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'R-squared: %f'</span> <span class="o">%</span> <span class="n">model</span><span class="p">.</span><span class="n">score</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">))</span>
    <span class="n">inc_prediction</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="n">df</span><span class="p">[</span><span class="s">'year'</span><span class="p">]</span><span class="o">==</span><span class="mi">2008</span><span class="p">][[</span><span class="s">'juneapp'</span><span class="p">,</span> <span class="s">'q2gdp'</span><span class="p">]])[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'incumbent prediction: %f'</span> <span class="o">%</span> <span class="n">inc_prediction</span><span class="p">)</span>

    <span class="n">prior</span> <span class="o">=</span> <span class="p">(</span><span class="mi">100</span> <span class="o">-</span> <span class="n">inc_prediction</span><span class="p">)</span> <span class="o">/</span> <span class="mf">100.</span>  <span class="c1"># Taking inverse and normalizing since we're predicting for Obama
</span>    <span class="k">print</span><span class="p">(</span><span class="s">'national prior %f'</span> <span class="o">%</span> <span class="n">prior</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">prior</span>

<span class="n">prior</span> <span class="o">=</span> <span class="n">fit_fundamentals_model</span><span class="p">()</span> <span class="c1"># 0.558804
</span></code></pre></div></div>
<p>And we get our prior of 55.8%.</p>

<p>To get state-specific priors, we can incorporate state level differences between % votes for Obama in the previous 2008 election, and apply those differences on priors</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">votes</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">'data/2008.csv'</span><span class="p">)</span>  <span class="c1"># 2008 results data
</span><span class="n">votes</span><span class="p">[</span><span class="s">'vote_share'</span><span class="p">]</span> <span class="o">=</span> <span class="n">votes</span><span class="p">[</span><span class="s">'total_count'</span><span class="p">]</span> <span class="o">/</span> <span class="n">votes</span><span class="p">[</span><span class="s">'total_count'</span><span class="p">].</span><span class="nb">sum</span><span class="p">()</span>
<span class="n">votes</span><span class="p">[</span><span class="s">'obama_diff'</span><span class="p">]</span> <span class="o">=</span> <span class="n">votes</span><span class="p">[</span><span class="s">'obama'</span><span class="p">]</span> <span class="o">-</span> <span class="p">(</span><span class="n">votes</span><span class="p">[</span><span class="s">'obama_count'</span><span class="p">].</span><span class="nb">sum</span><span class="p">()</span> <span class="o">/</span> <span class="n">votes</span><span class="p">[</span><span class="s">'total_count'</span><span class="p">].</span><span class="nb">sum</span><span class="p">())</span>

<span class="c1"># states is an array of state names
</span><span class="n">state_weights</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">([</span><span class="n">votes</span><span class="p">[</span><span class="n">votes</span><span class="p">[</span><span class="s">'state'</span><span class="p">]</span><span class="o">==</span><span class="n">s</span><span class="p">][</span><span class="s">'vote_share'</span><span class="p">].</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">states</span><span class="p">])</span>
<span class="n">state_prior</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">([</span><span class="n">votes</span><span class="p">[</span><span class="n">votes</span><span class="p">[</span><span class="s">'state'</span><span class="p">]</span><span class="o">==</span><span class="n">s</span><span class="p">][</span><span class="s">'obama_diff'</span><span class="p">].</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="n">prior</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">states</span><span class="p">])</span>
<span class="c1"># array([0.41770557, 0.41770557, 0.47770557, 0.63770557, 0.56770557,
#        0.63770557, 0.53770557, 0.49770557, 0.74770557, 0.56770557,
#        0.38770557, 0.64770557, 0.52770557, 0.43770557, 0.42770557,
#        0.64770557, 0.64770557, 0.60770557, 0.59770557, 0.56770557,
#        0.51770557, 0.49770557, 0.52770557, 0.47770557, 0.44770557,
#        0.56770557, 0.59770557, 0.59770557, 0.57770557, 0.65770557,
#        0.54770557, 0.36770557, 0.59770557, 0.56770557, 0.65770557,
#        0.47770557, 0.44770557, 0.46770557, 0.36770557, 0.55770557,
#        0.69770557, 0.60770557, 0.58770557, 0.45770557])
</span></code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">state_weights</code> will come into play later.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_polls</span><span class="p">():</span>
    <span class="n">polls</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">'data/all_polls_2012.csv'</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">'polls.shape'</span><span class="p">,</span> <span class="n">polls</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>
    <span class="n">polls</span><span class="p">[</span><span class="s">'end.date'</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">to_datetime</span><span class="p">(</span><span class="n">polls</span><span class="p">[</span><span class="s">'end.date'</span><span class="p">])</span>
    <span class="n">polls</span> <span class="o">=</span> <span class="n">polls</span><span class="p">[</span><span class="n">polls</span><span class="p">[</span><span class="s">'end.date'</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="s">'2012-08-01'</span><span class="p">]</span>
    <span class="n">election_date</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">Timestamp</span><span class="p">(</span><span class="s">'2012-11-06'</span><span class="p">)</span>

    <span class="c1"># Breaking out data into 5 day intervals
</span>    <span class="n">polls</span><span class="p">[</span><span class="s">'days_to_election'</span><span class="p">]</span> <span class="o">=</span> <span class="n">polls</span><span class="p">[</span><span class="s">'end.date'</span><span class="p">].</span><span class="nb">apply</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="p">(</span><span class="n">election_date</span> <span class="o">-</span> <span class="n">x</span><span class="p">).</span><span class="n">days</span><span class="p">)</span>
    <span class="n">polls</span><span class="p">[</span><span class="s">'day_idx'</span><span class="p">]</span> <span class="o">=</span> <span class="n">polls</span><span class="p">[</span><span class="s">'days_to_election'</span><span class="p">].</span><span class="nb">apply</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="o">//</span><span class="mi">5</span><span class="p">)</span>

    <span class="n">polls</span> <span class="o">=</span> <span class="n">polls</span><span class="p">[</span><span class="n">polls</span><span class="p">[</span><span class="s">'number.of.observations'</span><span class="p">].</span><span class="n">notnull</span><span class="p">()]</span>
    <span class="n">polls</span><span class="p">[</span><span class="s">'number.of.observations'</span><span class="p">]</span> <span class="o">=</span> <span class="n">polls</span><span class="p">[</span><span class="s">'number.of.observations'</span><span class="p">].</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
    <span class="n">polls</span><span class="p">[</span><span class="s">'obama'</span><span class="p">]</span> <span class="o">=</span> <span class="n">polls</span><span class="p">[</span><span class="s">'obama'</span><span class="p">].</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>
    <span class="n">polls</span><span class="p">[</span><span class="s">'obama_share'</span><span class="p">]</span> <span class="o">=</span> <span class="p">((</span><span class="n">polls</span><span class="p">[</span><span class="s">'obama'</span><span class="p">]</span> <span class="o">/</span> <span class="mf">100.</span><span class="p">)</span> <span class="o">*</span> <span class="n">polls</span><span class="p">[</span><span class="s">'number.of.observations'</span><span class="p">]).</span><span class="n">astype</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>

    <span class="n">state_polls</span> <span class="o">=</span> <span class="n">polls</span><span class="p">[</span><span class="n">polls</span><span class="p">[</span><span class="s">'state'</span><span class="p">]</span> <span class="o">!=</span> <span class="s">'--'</span><span class="p">]</span>
    <span class="n">national_polls</span> <span class="o">=</span> <span class="n">polls</span><span class="p">[</span><span class="n">polls</span><span class="p">[</span><span class="s">'state'</span><span class="p">]</span> <span class="o">==</span> <span class="s">'--'</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">state_polls</span><span class="p">,</span> <span class="n">national_polls</span>

<span class="n">state_polls</span><span class="p">,</span> <span class="n">national_polls</span> <span class="o">=</span> <span class="n">get_polls</span><span class="p">()</span>
</code></pre></div></div>

<p>There are both national and state polls in this dataset. National polls will be handled differently.</p>

<p>Then we just need to fit our Bayesian model to polling data.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">with</span> <span class="n">pm</span><span class="p">.</span><span class="n">Model</span><span class="p">()</span> <span class="k">as</span> <span class="n">model</span><span class="p">:</span>
    <span class="n">s_idx</span> <span class="o">=</span> <span class="n">pm</span><span class="p">.</span><span class="n">Data</span><span class="p">(</span><span class="s">'state_idx'</span><span class="p">,</span> <span class="n">state_polls</span><span class="p">[</span><span class="s">'state_idx'</span><span class="p">])</span>
    <span class="n">s_day</span> <span class="o">=</span> <span class="n">pm</span><span class="p">.</span><span class="n">Data</span><span class="p">(</span><span class="s">'state_day'</span><span class="p">,</span> <span class="n">state_polls</span><span class="p">[</span><span class="s">'day_idx'</span><span class="p">])</span>
    <span class="n">n_day</span> <span class="o">=</span> <span class="n">pm</span><span class="p">.</span><span class="n">Data</span><span class="p">(</span><span class="s">'national_day'</span><span class="p">,</span> <span class="n">national_polls</span><span class="p">[</span><span class="s">'day_idx'</span><span class="p">])</span>

    <span class="c1"># Defining state priors
</span>    <span class="n">s_prior</span> <span class="o">=</span> <span class="n">pm</span><span class="p">.</span><span class="n">Normal</span><span class="p">(</span><span class="s">'state_prior'</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">shape</span><span class="o">=</span><span class="n">num_states</span><span class="p">)</span> <span class="o">+</span> <span class="n">state_prior</span>
    <span class="n">s_sigma</span> <span class="o">=</span> <span class="n">pm</span><span class="p">.</span><span class="n">HalfCauchy</span><span class="p">(</span><span class="s">'state_sigma'</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">shape</span><span class="o">=</span><span class="n">num_states</span><span class="p">)</span>
    <span class="n">s_walk</span> <span class="o">=</span> <span class="n">GaussianRandomWalk</span><span class="p">(</span><span class="s">'state_walk'</span><span class="p">,</span> <span class="n">sigma</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">shape</span><span class="o">=</span><span class="p">(</span><span class="n">num_days</span><span class="p">,</span> <span class="n">num_states</span><span class="p">))</span>

    <span class="c1"># Defining national effect priors
</span>    <span class="n">n_walk</span> <span class="o">=</span> <span class="n">GaussianRandomWalk</span><span class="p">(</span><span class="s">'nation_walk'</span><span class="p">,</span> <span class="n">sigma</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">shape</span><span class="o">=</span><span class="n">num_days</span><span class="p">)</span>
    <span class="n">n_sigma</span> <span class="o">=</span> <span class="n">pm</span><span class="p">.</span><span class="n">HalfCauchy</span><span class="p">(</span><span class="s">'nation_sigma'</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>

    <span class="c1"># Combining state and national effects
</span>    <span class="n">state_effect</span> <span class="o">=</span> <span class="n">pm</span><span class="p">.</span><span class="n">Deterministic</span><span class="p">(</span><span class="s">'state_effect'</span><span class="p">,</span>
                                    <span class="n">pm</span><span class="p">.</span><span class="n">math</span><span class="p">.</span><span class="n">invlogit</span><span class="p">(</span>
                                        <span class="p">((</span><span class="n">s_walk</span> <span class="o">*</span> <span class="n">s_sigma</span><span class="p">)</span> <span class="o">+</span> <span class="n">s_prior</span><span class="p">)</span> <span class="o">+</span> \
                                        <span class="n">tt</span><span class="p">.</span><span class="n">stack</span><span class="p">(</span><span class="n">n_sigma</span> <span class="o">*</span> <span class="n">n_walk</span><span class="p">).</span><span class="n">T</span>
                                    <span class="p">))</span>

    <span class="c1"># State polls
</span>    <span class="n">state_poll_results</span> <span class="o">=</span> <span class="n">pm</span><span class="p">.</span><span class="n">Binomial</span><span class="p">(</span><span class="s">'state_poll_results'</span><span class="p">,</span>
                                     <span class="n">n</span><span class="o">=</span><span class="n">state_polls</span><span class="p">[</span><span class="s">'two_party_votes'</span><span class="p">],</span>
                                     <span class="n">p</span><span class="o">=</span><span class="n">state_effect</span><span class="p">[</span><span class="n">s_day</span><span class="p">,</span> <span class="n">s_idx</span><span class="p">],</span>
                                     <span class="n">observed</span><span class="o">=</span><span class="n">state_polls</span><span class="p">[</span><span class="s">'obama_votes'</span><span class="p">])</span>

    <span class="c1"># National polls
</span>    <span class="n">weighted_national_effects</span> <span class="o">=</span> <span class="n">pm</span><span class="p">.</span><span class="n">Deterministic</span><span class="p">(</span><span class="s">'weighted_national_effects'</span><span class="p">,</span> <span class="p">(</span><span class="n">state_effect</span> <span class="o">*</span> <span class="n">state_weights</span><span class="p">).</span><span class="nb">sum</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="n">state_weights</span><span class="p">.</span><span class="nb">sum</span><span class="p">())</span>
    <span class="n">national_poll_results</span> <span class="o">=</span> <span class="n">pm</span><span class="p">.</span><span class="n">Binomial</span><span class="p">(</span><span class="s">'national_poll_results'</span><span class="p">,</span>
                                        <span class="n">n</span><span class="o">=</span><span class="n">national_polls</span><span class="p">[</span><span class="s">'two_party_votes'</span><span class="p">],</span>
                                        <span class="n">p</span><span class="o">=</span><span class="n">weighted_national_effects</span><span class="p">[</span><span class="n">n_day</span><span class="p">],</span>
                                        <span class="n">observed</span><span class="o">=</span><span class="n">national_polls</span><span class="p">[</span><span class="s">'obama_votes'</span><span class="p">])</span>
</code></pre></div></div>

<p>Note the part <code class="language-plaintext highlighter-rouge">weighted_national_effects = pm.Deterministic(... (state_effect * state_weights)...)</code>, this is how the model is fit on national polling data.</p>

<p>Once the model is fit in <code class="language-plaintext highlighter-rouge">pymc3</code>, we can generate some charts from the resulting trace, such as the one below that shows the evolution state-level voter preference for Obama over time. The blue line is what the model learned, and the red dots are the results from the state polls.</p>

<p><img src="https://stphnma.github.io/assets/state_trace.png" alt="State Trace" /></p>

<p>Interestingly, we see a drop in Obama favorability around the beginning of October, right around the time of the Oct 3 debate, in which Romney supposedly <a href="https://www.pewresearch.org/politics/2012/10/08/romneys-strong-debate-performance-erases-obamas-lead/">performed well</a>. Also, all the state-level preferences seem to have the same trend – if you look closely, you can see they all peak in fall on the same dates, which speaks to how powerful the national level effect (\(\alpha_t\)) is in this model. (You can see the same pattern on the Economist’s <a href="https://github.com/TheEconomist/us-potus-model">github</a>)</p>

<p>From the trace, we can also get samples of the final vote probability for each state, and use that to generate the final election win probabilities.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">total_effect</span> <span class="o">=</span> <span class="n">trace</span><span class="p">[</span><span class="s">'state_effect'</span><span class="p">]</span>
<span class="n">total_votes_arr</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">total_effect</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]):</span>
    <span class="n">total_votes</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">j</span><span class="p">,</span> <span class="n">p</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">total_effect</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">]):</span>
        <span class="n">state</span> <span class="o">=</span> <span class="n">states</span><span class="p">[</span><span class="n">j</span><span class="p">]</span>
        <span class="k">if</span> <span class="n">p</span> <span class="o">&gt;</span> <span class="p">.</span><span class="mi">5</span><span class="p">:</span>
            <span class="n">total_votes</span> <span class="o">+=</span> <span class="n">electoral_votes</span><span class="p">[</span><span class="n">state</span><span class="p">]</span>
    <span class="n">total_votes_arr</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">total_votes</span><span class="p">)</span>
<span class="n">total_votes_arr</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">total_votes_arr</span><span class="p">)</span>

<span class="n">plt</span><span class="p">.</span><span class="n">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">[</span><span class="mi">10</span><span class="p">,</span> <span class="mi">6</span><span class="p">])</span>
<span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplot</span><span class="p">(</span><span class="mi">111</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">hist</span><span class="p">(</span><span class="n">total_votes_arr</span><span class="p">,</span> <span class="n">bins</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="p">.</span><span class="mi">8</span><span class="p">)</span>
<span class="n">win_pct</span> <span class="o">=</span> <span class="p">(</span><span class="n">total_votes_arr</span> <span class="o">&gt;=</span> <span class="mi">270</span><span class="p">).</span><span class="nb">sum</span><span class="p">()</span> <span class="o">/</span> <span class="nb">float</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">total_votes_arr</span><span class="p">))</span>
<span class="n">ax</span><span class="p">.</span><span class="n">axvline</span><span class="p">(</span><span class="mi">270</span><span class="p">,</span> <span class="n">ls</span><span class="o">=</span><span class="s">'--'</span><span class="p">,</span> <span class="n">c</span><span class="o">=</span><span class="s">'r'</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">set_title</span><span class="p">(</span><span class="s">'Obama Win Probability:  %d%%'</span> <span class="o">%</span> <span class="p">(</span><span class="n">win_pct</span> <span class="o">*</span> <span class="mi">100</span><span class="p">),</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">16</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="https://stphnma.github.io/assets/obama-win.png" alt="win" /></p>

<p>And that’s it! Overall it wasn’t too complicated, the most annoying part was waiting for the MCMC sampler to converge. I don’t get use <code class="language-plaintext highlighter-rouge">pymc3</code> much in my day job, so it was nice to play around with it some more.</p>

<h2 id="addendum">Addendum</h2>

<ul>
  <li>Again, huge props goes to the Economist for releasing and open sourcing their model. Without their documentation and code to guide me, I wouldn’t be able to do this post at all.</li>
  <li>I should emphasize, getting the MCMC sampler to run a few thousand samples is actually pretty annoying. I don’t remember exactly, but I think getting 10K samples took over 40 minutes. The dataset really isn’t that large, so I’m guessing this is just from the complexity of fitting a 51 random walk variables.</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">The curious case of hazard rates in the UFC</title><link href="https://stphnma.github.io/writing/ufc-hazard-rates/" rel="alternate" type="text/html" title="The curious case of hazard rates in the UFC" /><published>2017-11-29T00:00:00+00:00</published><updated>2017-11-29T00:00:00+00:00</updated><id>https://stphnma.github.io/writing/ufc-hazard-rates</id><content type="html" xml:base="https://stphnma.github.io/writing/ufc-hazard-rates/"><![CDATA[<script defer="" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
</script>

<h1 id="the-curious-case-of-hazard-rates-in-the-ufc">The curious case of hazard rates in the UFC</h1>

<p>I’ve recently scraped some UFC fights data, with the intention of applying some modeling on it in order to quantify fighter effectiveness. I’ve done similar work before applying a <a href="https://en.wikipedia.org/wiki/Proportional_hazards_model">Survival Analysis model</a> on hockey data to quantify player effectiveness, so I was curious to see what insights would come from applying the same type of approach on MMA fighters.</p>

<p>I was playing around with the Kaplan Meier estimate of the survival curve, using KO as the failure event, when I noticed something interesting.</p>

<p><img src="https://stphnma.github.io/assets/hazard_rate_duration.png" alt="Hazard Rate Duration" /></p>

<p>There’s a large spike in the hazard rate around 300 seconds, which is right around when the 1st round ends.</p>

<p>What does this all mean? Well, for anyone still reading past this point, let’s provide some background first.</p>

<h2 id="hold-on-whats-survival-analysis">Hold on, what’s Survival Analysis</h2>

<p><strong>Survival analysis</strong> is a field of statistics used to model the expected lifetime of an agent, before a “failure event” occurs. For example, a typical question in survival analysis would be “What’s the expected lifespan of prostate cancer patients?”, or “What’s the probability cancer patients survive up to 5 years?” etc.</p>

<p>The core of survival analysis lies within modelling the <a href="https://en.wikipedia.org/wiki/Survival_function">survival function</a>  S(t), which defines the probability that the object of interest will survive up to time t.</p>

\[S(t) = P[T \gt t]\]

<p>The complement to the survival function is the <strong>hazard rate</strong> h(t), which is defined as the instantaneous probability that the event occurs at time t.</p>

\[h(t) =  \lim \limits_{\delta \to 0} \frac{P[t \le T \le t+\delta  |  T \gt t]}{\delta}\]

<p>The cool thing about survival analysis is that we can define the “failure event” as any arbitrary event. For example, financial institutions use survival analysis to model defaults on loans, where the default is the failure event. E-commerce companies use it to model customer churn, where canceling a membership is the failure event.</p>

<p>In this case, since we’re dealing with fight data, I’m using knockouts (KO/TKO) as the failure event.</p>

<p>The data I’ve scraped comprises of the UFC fights (up until last month), including the duration of the fight, who the fighters were, and the outcome (KO/TKO, submission, referee decision, etc.).</p>

<p>To model KO survival as function of time, we simply need the total duration of each fight, and label for each fight indicating whether or not the fight ended in a KO/TKO or not.</p>

<h2 id="application">Application</h2>

<p>The lifelines library in python provides a nifty API for modeling survival functions. All you need to model the survival curve is a Pandas dataframe of fight duration and whether or not a KO/TKO occurred or not.</p>

<p>With the lifelines API, we can get a non-parametric estimate of the survival function through their <code class="language-plaintext highlighter-rouge">KaplanMeierFitter</code>.</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">from</span> <span class="nn">lifelines</span> <span class="kn">import</span> <span class="n">KaplanMeierFitter</span>
<span class="n">kmf</span> <span class="o">=</span> <span class="n">KaplanMeierFitter</span><span class="p">()</span>

<span class="n">kmf</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">surv_df_3</span><span class="p">[</span><span class="s">'fight_time'</span><span class="p">],</span> <span class="n">event_observed</span><span class="o">=</span><span class="n">surv_df_3</span><span class="p">[</span><span class="s">'KO'</span><span class="p">])</span>
<span class="n">kmf</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">title</span> <span class="o">=</span> <span class="s">"Probability of Surviving (a KO)"</span><span class="p">,</span> <span class="n">figsize</span> <span class="o">=</span> <span class="p">[</span><span class="mi">15</span><span class="p">,</span><span class="mi">7</span><span class="p">])</span></code></pre></figure>

<p>And we get something that looks like this:</p>

<p><img src="https://stphnma.github.io/assets/survival_curve_duration.png" alt="Survival Curve Duration" /></p>

<p>Interpretation of the curve is pretty straightforward. Each point represents the probabilty of surviving up to that point.</p>

<p>For example, there’s a ~82% chance of not getting KO’d within the first round, and ~70% chance of not getting KO’d within the first 2 rounds. Ultimately, it looks like there’s at least an 80% chance of not getting KO’d at all.</p>

<p>Notice the weird kink around 300 seconds? We’ll get back to that later.</p>

<p>Unfortunately, lifelines doesn’t provide a way to see the see the hazard function — however, we can approximate it by taking the shifted difference in ratio of the survival function.</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="n">survival_func</span> <span class="o">=</span> <span class="n">kmf</span><span class="p">.</span><span class="n">survival_function_</span>
<span class="n">shifted_survival_diff</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">survival_func</span><span class="o">/</span><span class="n">survival_func</span><span class="p">.</span> <span class="n">shift</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span>
<span class="n">hazard_empirical</span> <span class="o">=</span> <span class="n">shifted_survival_diff</span><span class="p">.</span><span class="n">div</span><span class="p">(</span><span class="n">survival_func</span><span class="p">.</span><span class="n">index</span><span class="p">.</span><span class="n">to_series</span><span class="p">(),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span></code></pre></figure>

<p>Essentially, what we are doing is this:</p>

\[h(t_{i+1}) = \frac{\widehat{S}(t_{i+1}) - \widehat{S}(t_i)} {t_{i+1} - t_i}  \approx \frac{P[t_i \le T \le t_{i+1}  |  T \gt t_i]}{t_{i+1} - t_i}\]

<p>Where \(\widehat{S}(t)\) is the empirical survival function from the Kaplan Meier estimate.</p>

<p>Calculating and plotting the hazard rate now give us this:</p>

<p><img src="https://stphnma.github.io/assets/hazard_rate_duration.png" alt="Hazard Rate Duration" /></p>

<p>Some things pop out:</p>
<ul>
  <li>the hazard rate of KO is highest in the beginning of the fight</li>
  <li>the hazard rate declines over time, but <strong>spikes up again around 300 seconds</strong> (remember the kink?)</li>
  <li>after that, it seems to stabilize and stay relatively constant</li>
</ul>

<p>It makes sense that the hazard rate is highest in the beginning, as that’s when fighters are still figuring each other out. In those early minutes, it’s also possible for one fighter to get lucky with a swing and knock the other guy/gal out.</p>

<p>Over time, fighters get into their rhythym, and are able to react better to their opponenets, which explains the stabilization in hazard rates.</p>

<p>However, the increase at 300 seconds is pretty odd. Recall that the hazard rate is defined as the instantaneous rate of failure, or the probability of failing at time t (given that the agent has survived up until then). This means that right around 300 seconds, the probability of a KO jumps by more than 3x.</p>

<p>And if we look at the distribution of fight times, there’s also a spike around 300 seconds.</p>

<p><img src="https://stphnma.github.io/assets/fight_times_hist.png" alt="Hazard Rate Duration" /></p>

<p>So for some reason, the probability of a KO ~ 300 seconds is much higher than other times.</p>

<h2 id="whats-going-on-here">What’s going on here?</h2>

<p>To be honest, I’m not sure.</p>

<p>It’s possible that fighters more likely to let their guard right before the 1st round ends. Or alternatively, fighters become more aggressive in the end of the first round, hoping to score a KO.</p>

<p>Or potentially there’s reason.</p>

<p>KO’s and TKO’s are judged by the sole discretion of the referee. The ref may call a KO/TKO if the fighter had been knocked unconscious, OR if the fighter had stopped defending himself/ herself. The second case is more ambiguous, and there have been complaints about early stoppage before. I wonder if a majority of these are early stoppages, and there’s some bias on the refs to call fights before the 1st round ends.</p>

<p>Conspiracy theories aside, what we can conclude is that fighters definitely need to keep their guard up in the seconds before the first round ends.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Creating a Moving Average chart in D3</title><link href="https://stphnma.github.io/writing/d3-moving-average/" rel="alternate" type="text/html" title="Creating a Moving Average chart in D3" /><published>2017-11-01T00:00:00+00:00</published><updated>2017-11-01T00:00:00+00:00</updated><id>https://stphnma.github.io/writing/d3-moving-average</id><content type="html" xml:base="https://stphnma.github.io/writing/d3-moving-average/"><![CDATA[<h1 id="moving-average-charts-in-d3">Moving Average Charts in D3</h1>

<p>Recently, a large portion of my job involves building D3.js-based dashboards to help track different metrics over time. Since I work in advertising, a large majority of those metrics comprise of click-thru rates (# ads clicked / # ads shown) or conversion rates (# ads that led to a conversion event / # ads shown) of different advertising strategies.</p>

<p>The issue with clicks and conversion is that they are pretty rare events, so on a daily basis their estimates can fluctuate quite a bit. So if we put something like click-thru rates by data in a visualization, it ends up looking like this:</p>

<p><img src="https://stphnma.github.io/assets/reg_timeseries.png" alt="Regular Time Series SS" /></p>

<p>This sucks. It’s incredibly hard to detect if there’s any clear trend in the data.
What’s the “average” click-thru?
And what’s the deal with the spikes? Are those just normal fluctuations, or is it because of low impression volume? Hard to say.</p>

<p>So how do we do this? We could just calculate the moving average in the data before calling the visualization, but that puts extra overhead on the data pre-processing. And where’s the fun in that?</p>

<p>To address this, let’s create module that will display both the underlying data, and a smoothed moving average.</p>

<h2 id="coding-time">Coding time</h2>

<p>Let’s start with a standard time series chart:</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript">  <span class="kd">var</span> <span class="nx">regularTimeSeries</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(){</span>

    <span class="nx">width</span> <span class="o">=</span> <span class="mi">450</span>
    <span class="nx">height</span> <span class="o">=</span> <span class="mi">150</span>
    <span class="nx">margin</span> <span class="o">=</span> <span class="p">{</span><span class="dl">'</span><span class="s1">top</span><span class="dl">'</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> <span class="dl">'</span><span class="s1">right</span><span class="dl">'</span><span class="p">:</span><span class="mi">10</span><span class="p">,</span> <span class="dl">'</span><span class="s1">bottom</span><span class="dl">'</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span> <span class="dl">'</span><span class="s1">left</span><span class="dl">'</span><span class="p">:</span><span class="mi">40</span><span class="p">}</span>

    <span class="kd">var</span> <span class="nx">yScale</span> <span class="o">=</span> <span class="nx">d3</span><span class="p">.</span><span class="nx">scale</span><span class="p">.</span><span class="nx">linear</span><span class="p">()</span>
              <span class="p">.</span><span class="nx">range</span><span class="p">([</span><span class="nx">height</span><span class="p">,</span> <span class="mi">0</span><span class="p">])</span>
    <span class="kd">var</span> <span class="nx">xScale</span> <span class="o">=</span> <span class="nx">d3</span><span class="p">.</span><span class="nx">time</span><span class="p">.</span><span class="nx">scale</span><span class="p">()</span>
              <span class="p">.</span><span class="nx">range</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span> <span class="nx">width</span><span class="p">])</span>

    <span class="kd">var</span> <span class="nx">line</span> <span class="o">=</span> <span class="nx">d3</span><span class="p">.</span><span class="nx">svg</span><span class="p">.</span><span class="nx">line</span><span class="p">()</span>
                <span class="p">.</span><span class="nx">x</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">){</span> <span class="k">return</span> <span class="nx">xScale</span><span class="p">(</span><span class="nx">d</span><span class="p">.</span><span class="nx">date</span><span class="p">)</span> <span class="p">})</span>
                <span class="p">.</span><span class="nx">y</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">){</span> <span class="k">return</span> <span class="nx">yScale</span><span class="p">(</span><span class="nx">d</span><span class="p">.</span><span class="nx">CTR</span><span class="p">)</span> <span class="p">})</span>


    <span class="nx">setupScales</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>
      <span class="nx">xScale</span><span class="p">.</span><span class="nx">domain</span><span class="p">([</span><span class="nx">d3</span><span class="p">.</span><span class="nx">min</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">){</span><span class="k">return</span> <span class="nx">d</span><span class="p">.</span><span class="nx">date</span><span class="p">}),</span> <span class="nx">d3</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">){</span><span class="k">return</span> <span class="nx">d</span><span class="p">.</span><span class="nx">date</span><span class="p">})</span> <span class="p">])</span>
      <span class="nx">yScale</span><span class="p">.</span><span class="nx">domain</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span> <span class="nx">d3</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">){</span><span class="k">return</span> <span class="nx">d</span><span class="p">.</span><span class="nx">CTR</span><span class="p">})</span> <span class="p">])</span>
    <span class="p">}</span>


    <span class="nx">chart</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">selection</span><span class="p">){</span>
      <span class="nx">selection</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>

        <span class="nx">setupScales</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span>

        <span class="nx">svg</span> <span class="o">=</span> <span class="nx">selection</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">svg</span><span class="dl">"</span><span class="p">)</span>
          <span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="dl">"</span><span class="s2">width</span><span class="dl">"</span><span class="p">,</span> <span class="nx">width</span> <span class="o">+</span> <span class="nx">margin</span><span class="p">.</span><span class="nx">left</span> <span class="o">+</span> <span class="nx">margin</span><span class="p">.</span><span class="nx">right</span><span class="p">)</span>
          <span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="dl">"</span><span class="s2">height</span><span class="dl">"</span><span class="p">,</span> <span class="nx">height</span> <span class="o">+</span> <span class="nx">margin</span><span class="p">.</span><span class="nx">top</span> <span class="o">+</span> <span class="nx">margin</span><span class="p">.</span><span class="nx">bottom</span><span class="p">)</span>

        <span class="nx">svg</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">path</span><span class="dl">"</span><span class="p">)</span>
            <span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="dl">'</span><span class="s1">class</span><span class="dl">'</span><span class="p">,</span><span class="dl">'</span><span class="s1">line</span><span class="dl">'</span><span class="p">)</span>
            <span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="dl">"</span><span class="s2">d</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">){</span> <span class="k">return</span> <span class="nx">line</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">})</span>
            <span class="p">.</span><span class="nx">style</span><span class="p">(</span><span class="dl">"</span><span class="s2">stroke</span><span class="dl">"</span><span class="p">,</span><span class="dl">"</span><span class="s2">steelblue</span><span class="dl">"</span><span class="p">)</span>
            <span class="p">.</span><span class="nx">style</span><span class="p">(</span><span class="dl">"</span><span class="s2">stroke-width</span><span class="dl">"</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
      <span class="p">})</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nx">chart</span>
  <span class="p">}</span>


  <span class="kd">var</span> <span class="nx">chart</span> <span class="o">=</span> <span class="nx">regularTimeSeries</span><span class="p">()</span>
  <span class="nx">d3</span><span class="p">.</span><span class="nx">select</span><span class="p">(</span><span class="dl">"</span><span class="s2">#chart</span><span class="dl">"</span><span class="p">).</span><span class="nx">data</span><span class="p">([</span><span class="nx">data</span><span class="p">]).</span><span class="nx">call</span><span class="p">(</span><span class="nx">chart</span><span class="p">)</span></code></pre></figure>

<p>This is the exact code used to generate the visualization above. It’s assuming that the input data looks like this:</p>

<p><img src="https://stphnma.github.io/assets/ctr_data.png" alt="Data SS" /></p>

<p>with CTR already calculated at each point in time. CTR is plotted on the y-axis, and date is on the x-axis (Note: I’m ignoring axis labels for this exercise.). The charting code is encapsulated in the <code class="language-plaintext highlighter-rouge">regularTimeSeries</code> function, and the last two lines generate the chart object and apply it on some data.</p>

<p>I’m skipping over most of the details of the code –  I based the structure on  on Mike Bostock’s <a href="https://bost.ocks.org/mike/chart/">reusuable charts framework</a>, and also the multitude of on line charts in D3, so if you’re new to D3, I suggest you check that out before continuing. I’ve personally found <a href="https://bl.ocks.org/d3noob">d3noob</a> and Scott Murray’s <a href="http://alignedleft.com/tutorials">tutorials</a> to be especially helpful.</p>

<p>Let’s modify this so it also plots a 7-day moving average, which should help smooth out the fluctuations.</p>

<p>We’ll start by adding some <strong>getter-setter</strong> methods that allow us to specify our x and y variables, and specify how to calculate the metric. This will help us recalculate the moving average CTR on each date.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="nx">chart</span><span class="p">.</span><span class="nx">xVar</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">value</span><span class="p">){</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">arguments</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="k">return</span> <span class="nx">x</span>
  <span class="nx">xVar</span> <span class="o">=</span> <span class="nx">value</span>
  <span class="k">return</span> <span class="nx">chart</span>
<span class="p">}</span>

<span class="nx">chart</span><span class="p">.</span><span class="nx">yVar</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">value</span><span class="p">){</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">arguments</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="k">return</span> <span class="nx">y</span>
  <span class="nx">yVar</span> <span class="o">=</span> <span class="nx">value</span>
  <span class="k">return</span> <span class="nx">chart</span>
<span class="p">}</span>

<span class="nx">chart</span><span class="p">.</span><span class="nx">yCalc</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">value</span><span class="p">){</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">arguments</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="k">return</span> <span class="nx">yCalc</span>
  <span class="nx">yCalc</span> <span class="o">=</span> <span class="nx">value</span>
  <span class="k">return</span> <span class="nx">chart</span>
<span class="p">}</span>
<span class="nx">chart</span><span class="p">.</span><span class="nx">windowSize</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">value</span><span class="p">){</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">arguments</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="k">return</span> <span class="nx">windowSize</span>
  <span class="nx">windowSize</span> <span class="o">=</span> <span class="nx">value</span>
  <span class="k">return</span> <span class="nx">chart</span>
<span class="p">}</span></code></pre></figure>

<p>I’ve also added a <code class="language-plaintext highlighter-rouge">windowSize</code> method that allows us to specify how many days to use for our moving average.</p>

<p>Now, our chart can be configured with those values specified, like so:</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">var</span> <span class="nx">chart</span> <span class="o">=</span> <span class="nx">regularTimeSeries</span><span class="p">()</span>
              <span class="p">.</span><span class="nx">xVar</span><span class="p">(</span><span class="dl">"</span><span class="s2">date</span><span class="dl">"</span><span class="p">)</span>
              <span class="p">.</span><span class="nx">yVar</span><span class="p">(</span><span class="dl">"</span><span class="s2">CTR</span><span class="dl">"</span><span class="p">)</span>
              <span class="p">.</span><span class="nx">yCalc</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">){</span><span class="k">return</span> <span class="nx">d</span><span class="p">.</span><span class="nx">imps</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">?</span> <span class="nx">d</span><span class="p">.</span><span class="nx">clicks</span><span class="o">/</span><span class="nx">d</span><span class="p">.</span><span class="nx">imps</span> <span class="p">:</span> <span class="mi">0</span><span class="p">})</span>
              <span class="p">.</span><span class="nx">windowSize</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span>
<span class="nx">d3</span><span class="p">.</span><span class="nx">select</span><span class="p">(</span><span class="dl">"</span><span class="s2">#chart</span><span class="dl">"</span><span class="p">).</span><span class="nx">data</span><span class="p">([</span><span class="nx">data</span><span class="p">]).</span><span class="nx">call</span><span class="p">(</span><span class="nx">chart</span><span class="p">)</span></code></pre></figure>

<p>With these specified, let’s add a function that will calculate a rolling sum of the data, based on the <code class="language-plaintext highlighter-rouge">windowSize</code>.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">function</span> <span class="nx">rollingSum</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>      
  
  <span class="nx">data</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">sort</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span><span class="nx">b</span><span class="p">){</span><span class="k">return</span> <span class="nx">d3</span><span class="p">.</span><span class="nx">ascending</span><span class="p">(</span><span class="nx">a</span><span class="p">[</span><span class="nx">xVar</span><span class="p">],</span><span class="nx">b</span><span class="p">[</span><span class="nx">xVar</span><span class="p">])})</span>

  <span class="kd">var</span> <span class="nx">summed</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">,</span> <span class="nx">i</span><span class="p">){</span>

    <span class="kd">var</span> <span class="nx">start</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="nx">i</span><span class="o">-</span><span class="nx">windowSize</span><span class="p">)</span>
    <span class="kd">var</span> <span class="nx">end</span> <span class="o">=</span> <span class="nx">i</span>
    <span class="kd">var</span> <span class="nx">sum</span> <span class="o">=</span> <span class="p">{}</span>

    <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">key</span> <span class="k">in</span> <span class="nx">d</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">d</span><span class="p">.</span><span class="nx">hasOwnProperty</span><span class="p">(</span><span class="nx">key</span><span class="p">))</span> <span class="p">{</span>
        <span class="nx">sum</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">key</span> <span class="o">!=</span> <span class="nx">xVar</span> <span class="p">?</span> <span class="nx">d3</span><span class="p">.</span><span class="nx">sum</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">start</span><span class="p">,</span> <span class="nx">end</span><span class="p">),</span> <span class="kd">function</span><span class="p">(</span><span class="nx">x</span><span class="p">)</span> <span class="p">{</span><span class="k">return</span> <span class="nx">x</span><span class="p">[</span><span class="nx">key</span><span class="p">]})</span> <span class="p">:</span> <span class="nx">d</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span>
      <span class="p">}</span> 
    <span class="p">}</span>

    <span class="k">return</span> <span class="nx">sum</span>
  <span class="p">})</span>
  <span class="k">return</span> <span class="nx">summed</span>
<span class="p">}</span></code></pre></figure>

<p>Taking that data as input, this will return an array that looks exactly the same, except that value at each data is the rolling sum of the past 7 days.</p>

<p>Now, we can easily apply the click-thru calculation on top of it, which we’ll also specify in a function:</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="kd">function</span> <span class="nx">calculateYValue</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>

  <span class="nx">data</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">){</span>
    <span class="nx">d</span><span class="p">[</span><span class="nx">yVar</span><span class="p">]</span> <span class="o">=</span> <span class="nx">yCalc</span><span class="p">(</span><span class="nx">d</span><span class="p">)</span>
    <span class="k">return</span> <span class="nx">d</span>
  <span class="p">})</span>
  <span class="k">return</span> <span class="nx">data</span>
<span class="p">}</span></code></pre></figure>

<p>Putting the above two functions together, we can come up with a new array of data that gives us a 7-day rolling sum of clicks and impressions, and a 7-day moving average of CTR based on these sums.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="nx">smoothedData</span> <span class="o">=</span> <span class="nx">smooth</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span>
<span class="nx">smoothedData</span> <span class="o">=</span> <span class="nx">calculateYValue</span><span class="p">(</span><span class="nx">smoothedData</span><span class="p">)</span></code></pre></figure>

<h4 id="math-interlude">Math Interlude:</h4>

<blockquote>
  <p>We could also calculate the moving average of CTR as (CTR_1 + CTR_2 + ….+ CTR_n) / n; however, this puts equal weight on each date. 
This is undesirable in this case, because the number ads shown differ greatly on a day-to-day basis – days when 10 ads are shown shouldn’t get the same weight as days with 10,000 ads shown. Calculating the average as the rolling sum of clicks / rolling sum of ads allows us to overcome this bias.</p>
</blockquote>

<p>The rest is pretty straightfoward – in <code class="language-plaintext highlighter-rouge">chart()</code>, we just need to add another <code class="language-plaintext highlighter-rouge">path</code> object that uses the <code class="language-plaintext highlighter-rouge">smoothedData</code> as its input source.</p>

<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="nx">chart</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">selection</span><span class="p">){</span>
  <span class="nx">selection</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">){</span>

    
    <span class="nx">smoothedData</span> <span class="o">=</span> <span class="nx">smooth</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span>
    <span class="nx">data</span> <span class="o">=</span> <span class="nx">calculateYValue</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span>
    <span class="nx">smoothedData</span> <span class="o">=</span> <span class="nx">calculateYValue</span><span class="p">(</span><span class="nx">smoothedData</span><span class="p">)</span>

    <span class="nx">setupScales</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span>

    <span class="nx">svg</span> <span class="o">=</span> <span class="nx">selection</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">svg</span><span class="dl">"</span><span class="p">)</span>
      <span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="dl">"</span><span class="s2">width</span><span class="dl">"</span><span class="p">,</span> <span class="nx">width</span> <span class="o">+</span> <span class="nx">margin</span><span class="p">.</span><span class="nx">left</span> <span class="o">+</span> <span class="nx">margin</span><span class="p">.</span><span class="nx">right</span><span class="p">)</span>
      <span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="dl">"</span><span class="s2">height</span><span class="dl">"</span><span class="p">,</span> <span class="nx">height</span> <span class="o">+</span> <span class="nx">margin</span><span class="p">.</span><span class="nx">top</span> <span class="o">+</span> <span class="nx">margin</span><span class="p">.</span><span class="nx">bottom</span><span class="p">)</span>

    <span class="nx">svg</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">path</span><span class="dl">"</span><span class="p">)</span>
        <span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="dl">'</span><span class="s1">class</span><span class="dl">'</span><span class="p">,</span><span class="dl">'</span><span class="s1">line</span><span class="dl">'</span><span class="p">)</span>
        <span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="dl">"</span><span class="s2">d</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">){</span> <span class="k">return</span> <span class="nx">line</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">})</span>
        <span class="p">.</span><span class="nx">style</span><span class="p">(</span><span class="dl">"</span><span class="s2">stroke</span><span class="dl">"</span><span class="p">,</span><span class="dl">"</span><span class="s2">steelblue</span><span class="dl">"</span><span class="p">)</span>
        <span class="p">.</span><span class="nx">style</span><span class="p">(</span><span class="dl">"</span><span class="s2">stroke-width</span><span class="dl">"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
        <span class="p">.</span><span class="nx">style</span><span class="p">(</span><span class="dl">"</span><span class="s2">opacity</span><span class="dl">"</span><span class="p">,</span> <span class="p">.</span><span class="mi">75</span><span class="p">)</span>

    <span class="nx">svg</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">path</span><span class="dl">"</span><span class="p">)</span>
        <span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="dl">'</span><span class="s1">class</span><span class="dl">'</span><span class="p">,</span><span class="dl">'</span><span class="s1">line</span><span class="dl">'</span><span class="p">)</span>
        <span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="dl">"</span><span class="s2">d</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">d</span><span class="p">){</span> <span class="k">return</span> <span class="nx">line</span><span class="p">(</span><span class="nx">smoothedData</span><span class="p">)</span> <span class="p">})</span>
        <span class="p">.</span><span class="nx">style</span><span class="p">(</span><span class="dl">"</span><span class="s2">stroke</span><span class="dl">"</span><span class="p">,</span><span class="dl">"</span><span class="s2">steelblue</span><span class="dl">"</span><span class="p">)</span>
        <span class="p">.</span><span class="nx">style</span><span class="p">(</span><span class="dl">"</span><span class="s2">stroke-width</span><span class="dl">"</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
  <span class="p">})</span>
<span class="p">}</span></code></pre></figure>

<p>We end up with a visualization that looks like this:</p>

<p><img src="https://stphnma.github.io/assets/ma_timeseries.png" alt="MA Time Series SS" /></p>

<p>I’ve added some styling to emphasize the moving average line more, but this is much better! It’s much more clear where the trends are, and keeps us from being reactive to random spikes or drops.</p>

<h2 id="conclusion">Conclusion</h2>

<p>This might be obvious to many, but smoothing your visualization matters helps a lot, especially when dealing with a lot of noise in the data.</p>

<p>Of course, you might need to be concerned about forcing trends that aren’t there, but that’s as discussion for another day ;).</p>

<p>Also, D3 is awesome!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Moving Average Charts in D3]]></summary></entry></feed>