title: Accelerating FizzBuzz with numpy
body: <p><strong>The FizzBuzz problem</strong>: Given a natural number <code>N &gt;= 0</code> print a sequence of numbers <code>0 - N</code> replacing every number that is divisible by <code>3</code> with the word <code>fizz</code>, every number that is divisible by <code>5</code> with the word <code>buzz</code>, and every number divisible by both <code>3</code> and <code>5</code> with the word <code>fizzbuzz</code>.</p>
  <p><a href="https://i.stack.imgur.com/ZzRoR.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/ZzRoR.png" alt="Timings of various implementations" /></a></p>
  <p>Numpy isn't excellent for this (it doesn't offer much acceleration for strings), but I figured it shouldn't be too horrible and, perhaps, it can beat plain python if used wisely. To my surprise, however, the opposite is the case for a naive implementation. <strong>Why is this, and how does one improve upon this?</strong></p>
  <hr />
  <p>Here is the code that I used to generate the timings. It includes a pure-python reference implementation, a naive numpy implementation, and a <code>numba.jit</code> variant, because I think it can act as a reasonable lower bound on performance.</p>
  <pre class="lang-py prettyprint-override"><code>import numpy as np
  import matplotlib.pyplot as plt
  import numba as nb
  import timeit
  
  
  def classic_fizzbuzz(N: int) -&gt; str:
      result = list()
  
      for idx in range(N):
          if idx % 3 == 0 and idx % 5 == 0:
              result.append(&quot;fizzbuzz&quot;)
          elif idx % 5 == 0:
              result.append(&quot;buzz&quot;)
          elif idx % 3 == 0:
              result.append(&quot;fizz&quot;)
          else:
              result.append(str(idx))
  
      return &quot; &quot;.join(result)
  
  
  def numpy_fizzbuzz(N: int) -&gt; str:
      integers = np.arange(N)
      result = np.arange(N).astype(str)
      result = np.where(integers % 3 == 0, &quot;fizz&quot;, result)
      result = np.where(integers % 5 == 0, &quot;buzz&quot;, result)
      result = np.where(integers % 15 == 0, &quot;fizzbuzz&quot;, result)
  
      return &quot; &quot;.join(result)
  
  
  @nb.jit(nopython=True)
  def numba_fizzbuzz(N:int) -&gt; str:
      result = list()
  
      for idx in range(N):
          if idx % 3 == 0 and idx % 5 == 0:
              result.append(&quot;fizzbuzz&quot;)
          elif idx % 5 == 0:
              result.append(&quot;buzz&quot;)
          elif idx % 3 == 0:
              result.append(&quot;fizz&quot;)
          else:
              result.append(str(idx))
  
      return &quot; &quot;.join(result)
  
  # do not measure initial compile time
  numba_fizzbuzz(20)
  
  def time_fizzbuzz(fn):
      repeats = 100
      times = list()
      N_values = np.linspace(0, int(1e4), 100, dtype=int)
      for idx in N_values:
          N = int(idx)
          times.append(timeit.timeit(lambda: fn(N), number=repeats) / repeats)
      
      return N_values, times
  
  fig, ax = plt.subplots(dpi=150)  # web-quality
  
  contendants = {
      &quot;classic&quot;: classic_fizzbuzz,
      &quot;numpy&quot;: numpy_fizzbuzz,
      &quot;numba&quot;: numba_fizzbuzz
  }
  
  for fn in contendants.values():
      ax.plot(*time_fizzbuzz(fn))
  
  ax.set_ylabel(&quot;Average Time (s)&quot;)
  ax.set_label(&quot;Input&quot;)
  fig.suptitle(
      &quot;Comparison of FizzBuzz implementations in Python.&quot;,
      # fontsize=&quot;medium&quot;,
  )
  ax.set_title(&quot;Timings for various input N (averaged over 100 runs each)&quot;, fontsize=&quot;small&quot;)
  
  ax.legend(
      contendants.keys(),
      loc=&quot;center left&quot;,
      bbox_to_anchor=(1, 0.5),
      title=&quot;Contestants&quot;,
  )
  
  fig.tight_layout()
  
  fig.savefig(&quot;timings.png&quot;)
  </code></pre>