{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Performance\n",
    "\n",
    "[Wes McKinney](https://twitter.com/wesmckinn), the creator of pandas, is kind of obsessed with performance. From micro-optimizations for element access, to [embedding](https://github.com/pydata/pandas/tree/master/pandas/src/klib) a fast hash table inside pandas, we all benefit from his and others' hard work.\n",
    "This post will focus mainly on making efficient use of pandas and NumPy.\n",
    "\n",
    "One thing I'll explicitly not touch on is storage formats.\n",
    "Performance is just one of many factors that go into choosing a storage format.\n",
    "Just know that pandas can talk to [many formats](http://pandas.pydata.org/pandas-docs/version/0.18.0/io.html), and the format that strikes the right balance between performance, portability, data-types, metadata handling, etc., is an [ongoing](http://blog.cloudera.com/blog/2016/03/feather-a-fast-on-disk-format-for-data-frames-for-r-and-python-powered-by-apache-arrow/) topic of discussion."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "import os\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "\n",
    "if int(os.environ.get(\"MODERN_PANDAS_EPUB\", 0)):\n",
    "    import prep # noqa\n",
    "\n",
    "sns.set_style('ticks')\n",
    "sns.set_context('talk')\n",
    "pd.options.display.max_rows = 10"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Constructors\n",
    "\n",
    "It's pretty common to have many similar sources (say a bunch of CSVs) that need to be combined into a single DataFrame. There are two routes to the same end:\n",
    "\n",
    "1. Initialize one DataFrame and append to that\n",
    "2. Make many smaller DataFrames and concatenate at the end\n",
    "\n",
    "For pandas, the second option is faster.\n",
    "DataFrame appends are expensive relative to a list append.\n",
    "Depending on the values, pandas might have to recast the data to a different type.\n",
    "And indexes are immutable, so each time you append pandas has to create an entirely new one.\n",
    "\n",
    "In the last section we downloaded a bunch of weather files, one per state, writing each to a separate CSV.\n",
    "One could imagine coming back later to read them in, using the following code.\n",
    "\n",
    "The idiomatic python way\n",
    "\n",
    "```python\n",
    "files = glob.glob('weather/*.csv')\n",
    "columns = ['station', 'date', 'tmpf', 'relh', 'sped', 'mslp',\n",
    "           'p01i', 'vsby', 'gust_mph', 'skyc1', 'skyc2', 'skyc3']\n",
    "\n",
    "# init empty DataFrame, like you might for a list\n",
    "weather = pd.DataFrame(columns=columns)\n",
    "\n",
    "for fp in files:\n",
    "    city = pd.read_csv(fp, columns=columns)\n",
    "    weather.append(city)\n",
    "```\n",
    "\n",
    "This is pretty standard code, quite similar to building up a list of tuples, say.\n",
    "The only nitpick is that you'd probably use a list-comprehension if you were just making a list.\n",
    "But we don't have special syntax for DataFrame-comprehensions (if only), so you'd fall back to the \"initialize empty container, append to said container\" pattern.\n",
    "\n",
    "But there's a better, pandorable, way\n",
    "\n",
    "```python\n",
    "files = glob.glob('weather/*.csv')\n",
    "weather_dfs = [pd.read_csv(fp, names=columns) for fp in files]\n",
    "weather = pd.concat(weather_dfs)\n",
    "```\n",
    "\n",
    "Subjectively this is cleaner and more beautiful.\n",
    "There's fewer lines of code.\n",
    "You don't have this extraneous detail of building an empty DataFrame.\n",
    "And objectively the pandorable way is faster, as we'll test next."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We'll define two functions for building an identical DataFrame. The first `append_df`, creates an empty DataFrame and appends to it. The second, `concat_df`,  creates many DataFrames, and concatenates them at the end. We also write a short decorator that runs the functions a handful of times and records the results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "size_per = 5000\n",
    "N = 100\n",
    "cols = list('abcd')\n",
    "\n",
    "def timed(n=30):\n",
    "    '''\n",
    "    Running a microbenchmark. Never use this.\n",
    "    '''\n",
    "    def deco(func):\n",
    "        def wrapper(*args, **kwargs):\n",
    "            timings = []\n",
    "            for i in range(n):\n",
    "                t0 = time.time()\n",
    "                func(*args, **kwargs)\n",
    "                t1 = time.time()\n",
    "                timings.append(t1 - t0)\n",
    "            return timings\n",
    "        return wrapper\n",
    "    return deco\n",
    "    \n",
    "@timed(60)\n",
    "def append_df():\n",
    "    '''\n",
    "    The pythonic (bad) way\n",
    "    '''\n",
    "    df = pd.DataFrame(columns=cols)\n",
    "    for _ in range(N):\n",
    "        df.append(pd.DataFrame(np.random.randn(size_per, 4), columns=cols))\n",
    "    return df\n",
    "\n",
    "@timed(60)\n",
    "def concat_df():\n",
    "    '''\n",
    "    The pandorabe (good) way\n",
    "    '''\n",
    "    dfs = [pd.DataFrame(np.random.randn(size_per, 4), columns=cols)\n",
    "           for _ in range(N)]\n",
    "    return pd.concat(dfs, ignore_index=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style>\n",
       "    .dataframe thead tr:only-child th {\n",
       "        text-align: right;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: left;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>level_0</th>\n",
       "      <th>Method</th>\n",
       "      <th>Time (s)</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0</td>\n",
       "      <td>Append</td>\n",
       "      <td>0.158359</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0</td>\n",
       "      <td>Concat</td>\n",
       "      <td>0.093890</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>1</td>\n",
       "      <td>Append</td>\n",
       "      <td>0.150050</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>1</td>\n",
       "      <td>Concat</td>\n",
       "      <td>0.092446</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2</td>\n",
       "      <td>Append</td>\n",
       "      <td>0.149565</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   level_0  Method  Time (s)\n",
       "0        0  Append  0.158359\n",
       "1        0  Concat  0.093890\n",
       "2        1  Append  0.150050\n",
       "3        1  Concat  0.092446\n",
       "4        2  Append  0.149565"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t_append = append_df()\n",
    "t_concat = concat_df()\n",
    "\n",
    "timings = (pd.DataFrame({\"Append\": t_append, \"Concat\": t_concat})\n",
    "             .stack()\n",
    "             .reset_index()\n",
    "             .rename(columns={0: 'Time (s)',\n",
    "                              'level_1': 'Method'}))\n",
    "timings.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<matplotlib.figure.Figure at 0x1184dd978>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.figure(figsize=(4, 6))\n",
    "sns.boxplot(x='Method', y='Time (s)', data=timings)\n",
    "sns.despine()\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Datatypes\n",
    "\n",
    "The pandas type system essentially [NumPy's](http://docs.scipy.org/doc/numpy/reference/arrays.scalars.html) with a few extensions (`categorical`, `datetime64` with timezone, `timedelta64`).\n",
    "An advantage of the DataFrame over a 2-dimensional NumPy array is that the DataFrame can have columns of various types within a single table.\n",
    "That said, each column should have a specific dtype; you don't want to be mixing bools with ints with strings within a single column.\n",
    "For one thing, this is slow.\n",
    "It forces the column to be have an `object` dtype (the fallback python-object container type), which means you don't get any of the type-specific optimizations in pandas or NumPy.\n",
    "For another, it means you're probably violating the maxims of tidy data, which we'll discuss next time.\n",
    "\n",
    "When should you have `object` columns?\n",
    "There are a few places where the NumPy / pandas type system isn't as rich as you might like.\n",
    "There's no integer NA (at the moment anyway), so if you have any missing values, represented by `NaN`, your otherwise integer column will be floats.\n",
    "There's also no `date` dtype (distinct from `datetime`).\n",
    "Consider the needs of your application: can you treat an integer `1` as `1.0`?\n",
    "Can you treat `date(2016, 1, 1)` as `datetime(2016, 1, 1, 0, 0)`?\n",
    "In my experience, this is rarely a problem other than when writing to something with a stricter schema like a database.\n",
    "But at that point it's fine to cast to one of the less performant types, since you're just not doing numeric operations anymore.\n",
    "\n",
    "The last case of `object` dtype data is text data.\n",
    "Pandas doesn't have any fixed-width string dtypes, so you're stuck with python objects.\n",
    "There is an important exception here, and that's low-cardinality text data, for which you'll want to use the `category` dtype (see below).\n",
    "\n",
    "If you have object data (either strings or python objects) that needs to be converted, checkout the [`to_numeric`](http://pandas.pydata.org/pandas-docs/version/0.18.0/generated/pandas.to_numeric.html), [`to_datetime`](http://pandas.pydata.org/pandas-docs/version/0.18.0/generated/pandas.to_datetime.html) and [`to_timedelta`](http://pandas.pydata.org/pandas-docs/version/0.18.0/generated/pandas.to_timedelta.html) methods."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Iteration, Apply, And Vectorization\n",
    "\n",
    "We know that [\"Python is slow\"](https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/) (scare  quotes since that statement is too broad to be meaningful).\n",
    "There are various steps that can be taken to improve your code's performance from relatively simple changes, to rewriting your code in a lower-level language, to trying to parallelize it.\n",
    "And while you might have many options, there's typically an order you would proceed in.\n",
    "\n",
    "First (and I know it's cliché to say so, but still) benchmark your code.\n",
    "Make sure you actually need to spend time optimizing it.\n",
    "There are [many](https://github.com/nvdv/vprof) [options](https://jiffyclub.github.io/snakeviz/) [for](https://github.com/rkern/line_profiler) [benchmarking](https://docs.python.org/3.5/library/timeit.html) and visualizing where things are slow.\n",
    "\n",
    "Second, consider your algorithm.\n",
    "Make sure you aren't doing more work than you need to.\n",
    "A common one I see is doing a full sort on an array, just to select the `N` largest or smallest items.\n",
    "Pandas has methods for that."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "df = pd.read_csv(\"data/347136217_T_ONTIME.csv\")\n",
    "delays = df['DEP_DELAY']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "112623    1480.0\n",
       "158136    1545.0\n",
       "152911    1934.0\n",
       "60246     1970.0\n",
       "59719     2755.0\n",
       "Name: DEP_DELAY, dtype: float64"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Select the 5 largest delays\n",
    "delays.nlargest(5).sort_values()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "300895   -59.0\n",
       "235921   -58.0\n",
       "197897   -56.0\n",
       "332533   -56.0\n",
       "344542   -55.0\n",
       "Name: DEP_DELAY, dtype: float64"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "delays.nsmallest(5).sort_values()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We follow up the `nlargest` or `nsmallest` with a sort (the result of `nlargest/smallest` is unordered), but it's much easier to sort 5 items that 500,000. The timings bear this out:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "29.9 ms ± 1.01 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n"
     ]
    }
   ],
   "source": [
    "%timeit delays.sort_values().tail(5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "7.85 ms ± 142 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
     ]
    }
   ],
   "source": [
    "%timeit delays.nlargest(5).sort_values()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\"Use the right algorithm\" is easy to say, but harder to apply in practice since you have to actually figure out the best algorithm to use.\n",
    "That one comes down to experience.\n",
    "\n",
    "Assuming you're at a spot that needs optimizing, and you've got the correct algorithm, *and* there isn't a readily available optimized version of what you need in pandas/numpy/scipy/scikit-learn/statsmodels/..., then what?\n",
    "\n",
    "The first place to turn is probably a vectorized NumPy implementation.\n",
    "Vectorization here means operating directly on arrays, rather than looping over lists scalars.\n",
    "This is generally much less work than rewriting it in something like Cython, and you can get pretty good results just by making *effective* use of NumPy and pandas.\n",
    "While not every operation can be vectorized, many can.\n",
    "\n",
    "Let's work through an example calculating the [Great-circle distance](https://en.wikipedia.org/wiki/Great-circle_distance) between airports.\n",
    "Grab the table of airport latitudes and longitudes from the [BTS website](http://www.transtats.bts.gov/DL_SelectFields.asp?Table_ID=288&DB_Short_Name=Aviation%20Support%20Tables) and extract it to a CSV."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from utils import download_airports\n",
    "import zipfile"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "if not os.path.exists(\"data/airports.csv.zip\"):\n",
    "    download_airports()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style>\n",
       "    .dataframe thead tr:only-child th {\n",
       "        text-align: right;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: left;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>LATITUDE</th>\n",
       "      <th>LONGITUDE</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>AIRPORT</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>8F3</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A03</th>\n",
       "      <td>58.457500</td>\n",
       "      <td>-154.023333</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A09</th>\n",
       "      <td>60.482222</td>\n",
       "      <td>-146.582222</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A18</th>\n",
       "      <td>63.541667</td>\n",
       "      <td>-150.993889</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A24</th>\n",
       "      <td>59.331667</td>\n",
       "      <td>-135.896667</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          LATITUDE   LONGITUDE\n",
       "AIRPORT                       \n",
       "8F3      33.623889 -101.240833\n",
       "A03      58.457500 -154.023333\n",
       "A09      60.482222 -146.582222\n",
       "A18      63.541667 -150.993889\n",
       "A24      59.331667 -135.896667"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "coord = (pd.read_csv(\"data/airports.csv.zip\", index_col=['AIRPORT'],\n",
    "                     usecols=['AIRPORT', 'LATITUDE', 'LONGITUDE'])\n",
    "           .groupby(level=0).first()\n",
    "           .dropna()\n",
    "           .sample(n=500, random_state=42)\n",
    "           .sort_index())\n",
    "\n",
    "coord.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For whatever reason, suppose we're interested in all the pairwise distances (I've limited it to just a sample of 500 airports to make this manageable.\n",
    "In the real world you *probably* don't need *all* the pairwise distances and would be better off with a [tree](http://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KDTree.html). Remember: think about what you actually need, and find the right algorithm for that).\n",
    "\n",
    "MultiIndexes have an alternative `from_product` constructor for getting the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product) of the arrays you pass in.\n",
    "We'll give it `coords.index` twice (to get its Cartesian product with itself).\n",
    "That gives a MultiIndex of all the combination.\n",
    "With some minor reshaping of `coords` we'll have a DataFrame with all the latitude/longitude pairs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style>\n",
       "    .dataframe thead tr:only-child th {\n",
       "        text-align: right;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: left;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th>LATITUDE_1</th>\n",
       "      <th>LONGITUDE_1</th>\n",
       "      <th>LATITUDE_2</th>\n",
       "      <th>LONGITUDE_2</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>origin</th>\n",
       "      <th>dest</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th rowspan=\"5\" valign=\"top\">8F3</th>\n",
       "      <th>8F3</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A03</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "      <td>58.457500</td>\n",
       "      <td>-154.023333</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A09</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "      <td>60.482222</td>\n",
       "      <td>-146.582222</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A18</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "      <td>63.541667</td>\n",
       "      <td>-150.993889</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A24</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "      <td>59.331667</td>\n",
       "      <td>-135.896667</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "             LATITUDE_1  LONGITUDE_1  LATITUDE_2  LONGITUDE_2\n",
       "origin dest                                                  \n",
       "8F3    8F3    33.623889  -101.240833   33.623889  -101.240833\n",
       "       A03    33.623889  -101.240833   58.457500  -154.023333\n",
       "       A09    33.623889  -101.240833   60.482222  -146.582222\n",
       "       A18    33.623889  -101.240833   63.541667  -150.993889\n",
       "       A24    33.623889  -101.240833   59.331667  -135.896667"
      ]
     },
     "execution_count": 45,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "idx = pd.MultiIndex.from_product([coord.index, coord.index],\n",
    "                                 names=['origin', 'dest'])\n",
    "\n",
    "pairs = pd.concat([coord.add_suffix('_1').reindex(idx, level='origin'),\n",
    "                   coord.add_suffix('_2').reindex(idx, level='dest')],\n",
    "                  axis=1)\n",
    "pairs.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "idx = idx[idx.get_level_values(0) <= idx.get_level_values(1)]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "125250"
      ]
     },
     "execution_count": 48,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "len(idx)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We'll break that down a bit, but don't lose sight of the real target: our great-circle distance calculation.\n",
    "\n",
    "The `add_suffix` (and `add_prefix`) method is handy for quickly renaming the columns."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style>\n",
       "    .dataframe thead tr:only-child th {\n",
       "        text-align: right;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: left;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>LATITUDE_1</th>\n",
       "      <th>LONGITUDE_1</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>AIRPORT</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>8F3</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A03</th>\n",
       "      <td>58.457500</td>\n",
       "      <td>-154.023333</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A09</th>\n",
       "      <td>60.482222</td>\n",
       "      <td>-146.582222</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A18</th>\n",
       "      <td>63.541667</td>\n",
       "      <td>-150.993889</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A24</th>\n",
       "      <td>59.331667</td>\n",
       "      <td>-135.896667</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         LATITUDE_1  LONGITUDE_1\n",
       "AIRPORT                         \n",
       "8F3       33.623889  -101.240833\n",
       "A03       58.457500  -154.023333\n",
       "A09       60.482222  -146.582222\n",
       "A18       63.541667  -150.993889\n",
       "A24       59.331667  -135.896667"
      ]
     },
     "execution_count": 49,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "coord.add_suffix('_1').head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Alternatively you could use the more general `.rename` like `coord.rename(columns=lambda x: x + '_1')`.\n",
    "\n",
    "Next, we have the `reindex`.\n",
    "Like I mentioned in the prior chapter, indexes are crucial to pandas.\n",
    "`.reindex` is all about aligning a Series or DataFrame to a given index.\n",
    "In this case we use `.reindex` to align our original DataFrame to the new\n",
    "MultiIndex of combinations.\n",
    "By default, the output will have the original value if that index label was already present, and `NaN` otherwise.\n",
    "If we just called `coord.reindex(idx)`, with no additional arguments, we'd get a DataFrame of all `NaN`s."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style>\n",
       "    .dataframe thead tr:only-child th {\n",
       "        text-align: right;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: left;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th>LATITUDE</th>\n",
       "      <th>LONGITUDE</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>origin</th>\n",
       "      <th>dest</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th rowspan=\"5\" valign=\"top\">8F3</th>\n",
       "      <th>8F3</th>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A03</th>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A09</th>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A18</th>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A24</th>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "             LATITUDE  LONGITUDE\n",
       "origin dest                     \n",
       "8F3    8F3        NaN        NaN\n",
       "       A03        NaN        NaN\n",
       "       A09        NaN        NaN\n",
       "       A18        NaN        NaN\n",
       "       A24        NaN        NaN"
      ]
     },
     "execution_count": 50,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "coord.reindex(idx).head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "That's because there weren't any values of `idx` that were in `coord.index`,\n",
    "which makes sense since `coord.index` is just a regular one-level Index, while `idx` is a MultiIndex.\n",
    "We use the `level` keyword to handle the transition from the original single-level Index, to the two-leveled `idx`.\n",
    "\n",
    "> `level` : int or name\n",
    ">\n",
    "  Broadcast across a level, matching Index values on the\n",
    "    passed MultiIndex level\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style>\n",
       "    .dataframe thead tr:only-child th {\n",
       "        text-align: right;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: left;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th>LATITUDE</th>\n",
       "      <th>LONGITUDE</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>origin</th>\n",
       "      <th>dest</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th rowspan=\"5\" valign=\"top\">8F3</th>\n",
       "      <th>8F3</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A03</th>\n",
       "      <td>58.457500</td>\n",
       "      <td>-154.023333</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A09</th>\n",
       "      <td>60.482222</td>\n",
       "      <td>-146.582222</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A18</th>\n",
       "      <td>63.541667</td>\n",
       "      <td>-150.993889</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A24</th>\n",
       "      <td>59.331667</td>\n",
       "      <td>-135.896667</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "              LATITUDE   LONGITUDE\n",
       "origin dest                       \n",
       "8F3    8F3   33.623889 -101.240833\n",
       "       A03   58.457500 -154.023333\n",
       "       A09   60.482222 -146.582222\n",
       "       A18   63.541667 -150.993889\n",
       "       A24   59.331667 -135.896667"
      ]
     },
     "execution_count": 51,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "coord.reindex(idx, level='dest').head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you ever need to do an operation that mixes regular single-level indexes with Multilevel Indexes, look for a level keyword argument.\n",
    "For example, all the arithmatic methods (`.mul`, `.add`, etc.) have them.\n",
    "\n",
    "This is a bit wasteful since the distance from airport `A` to `B` is the same as `B` to `A`.\n",
    "We could easily fix this with a `idx = idx[idx.get_level_values(0) <= idx.get_level_values(1)]`, but we'll ignore that for now.\n",
    "\n",
    "\n",
    "Quick tangent, I got some... let's say skepticism, on my last piece about the value of indexes.\n",
    "Here's an alternative version for the skeptics"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from itertools import product, chain\n",
    "coord2 = coord.reset_index()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style>\n",
       "    .dataframe thead tr:only-child th {\n",
       "        text-align: right;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: left;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th>LATITUDE_1</th>\n",
       "      <th>LONGITUDE_1</th>\n",
       "      <th>LATITUDE_1</th>\n",
       "      <th>LONGITUDE_2</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>origin</th>\n",
       "      <th>dest</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th rowspan=\"5\" valign=\"top\">8F3</th>\n",
       "      <th>8F3</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A03</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "      <td>58.457500</td>\n",
       "      <td>-154.023333</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A09</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "      <td>60.482222</td>\n",
       "      <td>-146.582222</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A18</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "      <td>63.541667</td>\n",
       "      <td>-150.993889</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>A24</th>\n",
       "      <td>33.623889</td>\n",
       "      <td>-101.240833</td>\n",
       "      <td>59.331667</td>\n",
       "      <td>-135.896667</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "             LATITUDE_1  LONGITUDE_1  LATITUDE_1  LONGITUDE_2\n",
       "origin dest                                                  \n",
       "8F3    8F3    33.623889  -101.240833   33.623889  -101.240833\n",
       "       A03    33.623889  -101.240833   58.457500  -154.023333\n",
       "       A09    33.623889  -101.240833   60.482222  -146.582222\n",
       "       A18    33.623889  -101.240833   63.541667  -150.993889\n",
       "       A24    33.623889  -101.240833   59.331667  -135.896667"
      ]
     },
     "execution_count": 53,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "x = product(coord2.add_suffix('_1').itertuples(index=False),\n",
    "            coord2.add_suffix('_2').itertuples(index=False))\n",
    "y = [list(chain.from_iterable(z)) for z in x]\n",
    "\n",
    "df2 = (pd.DataFrame(y, columns=['origin', 'LATITUDE_1', 'LONGITUDE_1',\n",
    "                                'dest', 'LATITUDE_1', 'LONGITUDE_2'])\n",
    "       .set_index(['origin', 'dest']))\n",
    "df2.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It's also readable (it's Python after all), though a bit slower.\n",
    "To me the `.reindex` method seems more natural.\n",
    "My thought process was, \"I need all the combinations of origin & destination (`MultiIndex.from_product`).\n",
    "Now I need to align this original DataFrame to this new MultiIndex (`coords.reindex`).\"\n",
    "\n",
    "With that diversion out of the way, let's turn back to our great-circle distance calculation.\n",
    "Our first implementation is pure python.\n",
    "The algorithm itself isn't too important, all that matters is that we're doing math operations on scalars."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import math\n",
    "\n",
    "def gcd_py(lat1, lng1, lat2, lng2):\n",
    "    '''\n",
    "    Calculate great circle distance between two points.\n",
    "    http://www.johndcook.com/blog/python_longitude_latitude/\n",
    "    \n",
    "    Parameters\n",
    "    ----------\n",
    "    lat1, lng1, lat2, lng2: float\n",
    "    \n",
    "    Returns\n",
    "    -------\n",
    "    distance:\n",
    "      distance from ``(lat1, lng1)`` to ``(lat2, lng2)`` in kilometers.\n",
    "    '''\n",
    "    # python2 users will have to use ascii identifiers (or upgrade)\n",
    "    degrees_to_radians = math.pi / 180.0\n",
    "    ϕ1 = (90 - lat1) * degrees_to_radians\n",
    "    ϕ2 = (90 - lat2) * degrees_to_radians\n",
    "    \n",
    "    θ1 = lng1 * degrees_to_radians\n",
    "    θ2 = lng2 * degrees_to_radians\n",
    "    \n",
    "    cos = (math.sin(ϕ1) * math.sin(ϕ2) * math.cos(θ1 - θ2) +\n",
    "           math.cos(ϕ1) * math.cos(ϕ2))\n",
    "    # round to avoid precision issues on identical points causing ValueErrors\n",
    "    cos = round(cos, 8)\n",
    "    arc = math.acos(cos)\n",
    "    return arc * 6373  # radius of earth, in kilometers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The second implementation uses NumPy.\n",
    "Aside from numpy having a builtin `deg2rad` convenience function (which is probably a bit slower than multiplying by a constant $\\frac{\\pi}{180}$), basically all we've done is swap the `math` prefix for `np`.\n",
    "Thanks to NumPy's broadcasting, we can write code that works on scalars or arrays of conformable shape."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def gcd_vec(lat1, lng1, lat2, lng2):\n",
    "    '''\n",
    "    Calculate great circle distance.\n",
    "    http://www.johndcook.com/blog/python_longitude_latitude/\n",
    "    \n",
    "    Parameters\n",
    "    ----------\n",
    "    lat1, lng1, lat2, lng2: float or array of float\n",
    "    \n",
    "    Returns\n",
    "    -------\n",
    "    distance:\n",
    "      distance from ``(lat1, lng1)`` to ``(lat2, lng2)`` in kilometers.\n",
    "    '''\n",
    "    # python2 users will have to use ascii identifiers\n",
    "    ϕ1 = np.deg2rad(90 - lat1)\n",
    "    ϕ2 = np.deg2rad(90 - lat2)\n",
    "    \n",
    "    θ1 = np.deg2rad(lng1)\n",
    "    θ2 = np.deg2rad(lng2)\n",
    "    \n",
    "    cos = (np.sin(ϕ1) * np.sin(ϕ2) * np.cos(θ1 - θ2) +\n",
    "           np.cos(ϕ1) * np.cos(ϕ2))\n",
    "    arc = np.arccos(cos)\n",
    "    return arc * 6373"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To use the python version on our DataFrame, we can either iterate..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 823 ms, sys: 9.51 ms, total: 833 ms\n",
      "Wall time: 832 ms\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "origin  dest\n",
       "8F3     8F3         0.000000\n",
       "        A03      4744.967448\n",
       "        A09      4407.533212\n",
       "        A18      4744.593127\n",
       "        A24      3820.092688\n",
       "                    ...     \n",
       "ZZU     YUY     12643.665960\n",
       "        YYL     13687.592278\n",
       "        ZBR      4999.647307\n",
       "        ZXO     14925.531303\n",
       "        ZZU         0.000000\n",
       "Length: 250000, dtype: float64"
      ]
     },
     "execution_count": 56,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "%%time\n",
    "pd.Series([gcd_py(*x) for x in pairs.itertuples(index=False)],\n",
    "          index=pairs.index)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Or use `DataFrame.apply`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 14.4 s, sys: 58.6 ms, total: 14.5 s\n",
      "Wall time: 14.5 s\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "r = pairs.apply(lambda x: gcd_py(x['LATITUDE_1'], x['LONGITUDE_1'],\n",
    "                                 x['LATITUDE_2'], x['LONGITUDE_2']), axis=1);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But as you can see, you don't want to use apply, especially with `axis=1` (calling the function on each row).  It's doing a lot more work handling dtypes in the background, and trying to infer the correct output shape that are pure overhead in this case. On top of that, it has to essentially use a for loop internally.\n",
    "\n",
    "You *rarely* want to use `DataFrame.apply` and almost never should use it with `axis=1`. Better to write functions that take arrays, and pass those in directly. Like we did with the vectorized version"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 34.9 ms, sys: 21.1 ms, total: 56 ms\n",
      "Wall time: 38.9 ms\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/taugspurger/miniconda3/envs/modern-pandas/lib/python3.6/site-packages/ipykernel_launcher.py:24: RuntimeWarning: invalid value encountered in arccos\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "r = gcd_vec(pairs['LATITUDE_1'], pairs['LONGITUDE_1'],\n",
    "            pairs['LATITUDE_2'], pairs['LONGITUDE_2'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "origin  dest\n",
       "8F3     8F3        0.000000\n",
       "        A03     4744.967484\n",
       "        A09     4407.533240\n",
       "        A18     4744.593111\n",
       "        A24     3820.092639\n",
       "dtype: float64"
      ]
     },
     "execution_count": 60,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "r.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "I try not to use the word \"easy\" when teaching, but that optimization was easy right?\n",
    "Why then, do I come across uses of `apply`, in my code and others', even when the vectorized version is available?\n",
    "The difficulty lies in knowing about broadcasting, and seeing where to apply it.\n",
    "\n",
    "For example, the README for [lifetimes](https://github.com/CamDavidsonPilon/lifetimes) (by Cam Davidson Pilon, also author of [Bayesian Methods for Hackers](https://github.com/CamDavidsonPilon/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers), [lifelines](https://github.com/CamDavidsonPilon/lifelines), and [Data Origami](https://dataorigami.net)) used to have an example of passing [this method](https://github.com/CamDavidsonPilon/lifetimes/blob/5b4f7de0720413b6951ac0a4b0082bd50255a231/lifetimes/estimation.py#L249) into a `DataFrame.apply`.\n",
    "\n",
    "```python\n",
    "data.apply(lambda r: bgf.conditional_expected_number_of_purchases_up_to_time(\n",
    "    t, r['frequency'], r['recency'], r['T']), axis=1\n",
    ")\n",
    "```\n",
    "\n",
    "If you look at the function [I linked to](https://github.com/CamDavidsonPilon/lifetimes/blob/5b4f7de0720413b6951ac0a4b0082bd50255a231/lifetimes/estimation.py#L249), it's doing a fairly complicated computation involving a negative log likelihood and the Gamma function from `scipy.special`.\n",
    "But crucially, it was already vectorized.\n",
    "We were able to change the example to just pass the arrays (Series in this case) into the function, rather than applying the function to each row.\n",
    "\n",
    "```python\n",
    "bgf.conditional_expected_number_of_purchases_up_to_time(\n",
    "    t, data['frequency'], data['recency'], data['T']\n",
    ")\n",
    "```\n",
    "\n",
    "This got us another 30x speedup on the example dataset.\n",
    "I bring this up because it's very natural to have to translate an equation to code and think, \"Ok now I need to apply this function to each row\", so you reach for `DataFrame.apply`.\n",
    "See if you can just pass in the NumPy array or Series itself instead.\n",
    "\n",
    "Not all operations this easy to vectorize.\n",
    "Some operations are iterative by nature, and rely on the results of surrounding computations to proceed. In cases like this you can hope that one of the scientific python libraries has implemented it efficiently for you, or write your own solution using Numba / C / Cython / Fortran.\n",
    "\n",
    "Other examples take a bit more thought or knowledge to vectorize.\n",
    "Let's look at [this](http://nbviewer.jupyter.org/github/jreback/pydata2015-london/blob/master/notebooks/idioms.ipynb)\n",
    "example, taken from Jeff Reback's PyData London talk, that groupwise normalizes a dataset by subtracting the mean and dividing by the standard deviation for each group."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "def create_frame(n, n_groups):\n",
    "    # just setup code, not benchmarking this\n",
    "    stamps = pd.date_range('20010101', periods=n, freq='ms')\n",
    "    random.shuffle(stamps.values)    \n",
    "    return pd.DataFrame({'name': np.random.randint(0,n_groups,size=n),\n",
    "                         'stamp': stamps,\n",
    "                         'value': np.random.randint(0,n,size=n),\n",
    "                         'value2': np.random.randn(n)})\n",
    "\n",
    "\n",
    "df = create_frame(1000000,10000)\n",
    "\n",
    "def f_apply(df):\n",
    "    # Typical transform\n",
    "    return df.groupby('name').value2.apply(lambda x: (x-x.mean())/x.std())\n",
    "\n",
    "def f_unwrap(df):\n",
    "    # \"unwrapped\"\n",
    "    g = df.groupby('name').value2\n",
    "    v = df.value2\n",
    "    return (v-g.transform(np.mean))/g.transform(np.std)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Timing it we see that the \"unwrapped\" version, get's quite a bit better performance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "%timeit f_apply(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "%timeit f_unwrap(df)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pandas GroupBy objects intercept calls for common functions like mean, sum, etc. and substitutes them with optimized Cython versions.\n",
    "So the unwrapped `.transform(np.mean)` and `.transform(np.std)` are fast, while the `x.mean` and `x.std` in the `.apply(lambda x: x - x.mean()/x.std())` aren't.\n",
    "\n",
    "`Groupby.apply` is always going to be around, beacuse it offers maximum flexibility. If you need to [fit a model on each group and create additional columns in the process](http://stackoverflow.com/q/35924126/1889400), it can handle that. It just might not be the fastest (which may be OK sometimes).\n",
    "\n",
    "This last example is admittedly niche.\n",
    "I'd like to think that there aren't too many places in pandas where the natural thing to do `.transform((x - x.mean()) / x.std())` is slower than the less obvious alternative.\n",
    "Ideally the user wouldn't have to know about GroupBy having special fast implementations of common methods.\n",
    "But that's where we are now."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Categoricals"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "Thanks to some great work by [Jan Schulz](https://twitter.com/janschulz), [Jeff Reback](https://twitter.com/janschulz), and others, pandas 0.15 gained a new [Categorical](http://pandas.pydata.org/pandas-docs/version/0.18.0/categorical.html) data type. Categoricals are nice for many reasons beyond just efficiency, but we'll focus on that here.\n",
    "\n",
    "Categoricals are an efficient way of representing data (typically strings) that have a low *cardinality*, i.e. relatively few distinct values relative to the size of the array. Internally, a Categorical stores the categories once, and an array of `codes`, which are just integers that indicate which category belongs there. Since it's cheaper to store a `code` than a `category`, we save on memory (shown next).\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import string\n",
    "\n",
    "s = pd.Series(np.random.choice(list(string.ascii_letters), 100000))\n",
    "print('{:0.2f} KB'.format(s.memory_usage(index=False) / 1000))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "c = s.astype('category')\n",
    "print('{:0.2f} KB'.format(c.memory_usage(index=False) / 1000))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Beyond saving memory, having codes and a fixed set of categories offers up a bunch of algorithmic optimizations that pandas and others can take advantage of.\n",
    "\n",
    "[Matthew Rocklin](https://twitter.com/mrocklin) has a very nice [post](http://matthewrocklin.com/blog/work/2015/06/18/Categoricals) on using categoricals, and optimizing code in general."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Going Further\n",
    "\n",
    "The pandas documentation has a section on [enhancing performance](http://pandas.pydata.org/pandas-docs/version/0.18.0/enhancingperf.html), focusing on using Cython or `numba` to speed up a computation. I've focused more on the lower-hanging fruit of picking the right algorithm, vectorizing your code, and using pandas or numpy more effetively. There are further optimizations availble if these aren't enough."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "This post was more about how to make effective use of numpy and pandas, than writing your own highly-optimized code.\n",
    "In my day-to-day work of data analysis it's not worth the time to write and compile a cython extension.\n",
    "I'd rather rely on pandas to be fast at what matters (label lookup on large arrays, factorizations for groupbys and merges, numerics).\n",
    "If you want to learn more about what pandas does to make things fast, checkout Jeff Tratner' talk from PyData Seattle [talk](http://www.jeffreytratner.com/slides/pandas-under-the-hood-pydata-seattle-2015.pdf) on pandas' internals.\n",
    "\n",
    "Next time we'll look at a differnt kind of optimization: using the Tidy Data principles to facilitate efficient data analysis.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
