Skip to content

Instantly share code, notes, and snippets.

@shoyer
Created December 27, 2015 15:00
Show Gist options
  • Save shoyer/c939325f509d7c027949 to your computer and use it in GitHub Desktop.
Save shoyer/c939325f509d7c027949 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"%matplotlib inline\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import pandas as pd\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# `Interval`, `IntervalIndex` and API changes to `cut`/`qcut`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Changes to `cut`/`qcut`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you explicitly supply `bins`, these functions work very similarly:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": false,
"scrolled": false
},
"outputs": [
{
"data": {
"text/plain": [
"0 (-5, 0]\n",
"1 (0, 5]\n",
"2 (0, 5]\n",
"3 (0, 5]\n",
"4 (0, 5]\n",
"5 (0, 5]\n",
"6 (5, 10]\n",
"7 (5, 10]\n",
"8 (5, 10]\n",
"9 (5, 10]\n",
"dtype: object"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"s = pd.Series(np.arange(10))\n",
"pd.cut(s, [-5, 0, 5, 10])"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"(-5, 0] 1\n",
"(0, 5] 5\n",
"(5, 10] 4\n",
"dtype: int64"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"s.groupby(pd.cut(s, [-5, 0, 5, 10])).count()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"However, under the hood, bins are now represented by `Interval` objects instead of strings:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"array([Interval(-5, 0, closed='right'), Interval(0, 5, closed='right'),\n",
" Interval(0, 5, closed='right'), Interval(0, 5, closed='right'),\n",
" Interval(0, 5, closed='right'), Interval(0, 5, closed='right'),\n",
" Interval(5, 10, closed='right'), Interval(5, 10, closed='right'),\n",
" Interval(5, 10, closed='right'), Interval(5, 10, closed='right')], dtype=object)"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"pd.cut(s, [-5, 0, 5, 10]).values"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Unfortunately, pandas (and numpy) don't yet have a native dtype for interval data, so they are stored in the form of `dtype=object` arrays. (However, there is an `IntervalIndex`, which we'll get to later.)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The main breaking change for `cut` and `qcut` is that their result is no longer a `Categorical`, unless you pass in categorical labels:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"0 -5 - 0\n",
"1 0 - 5\n",
"2 0 - 5\n",
"3 0 - 5\n",
"4 0 - 5\n",
"5 0 - 5\n",
"6 5 - 10\n",
"7 5 - 10\n",
"8 5 - 10\n",
"9 5 - 10\n",
"dtype: object"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# not categorical\n",
"pd.cut(s, [-5, 0, 5, 10], labels=['-5 - 0', '0 - 5', '5 - 10'])"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"0 -5 - 0\n",
"1 0 - 5\n",
"2 0 - 5\n",
"3 0 - 5\n",
"4 0 - 5\n",
"5 0 - 5\n",
"6 5 - 10\n",
"7 5 - 10\n",
"8 5 - 10\n",
"9 5 - 10\n",
"dtype: category\n",
"Categories (3, object): [-5 - 0, 0 - 5, 5 - 10]"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# categorical\n",
"pd.cut(s, [-5, 0, 5, 10], labels=pd.Categorical(['-5 - 0', '0 - 5', '5 - 10']))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## The new `Interval` type"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Interval objects are useful. They keep track of their bounds in a way you can access later:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"intv = pd.Interval(0, 10)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"Interval(0, 10, closed='right')"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"intv"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"0"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"intv.left"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"10"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"intv.right"
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"'right'"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"intv.closed"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": false
},
"source": [
"Intervals have several derived properties:"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"5.0"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"intv.mid"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"(True, False, False, True)"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"intv.open_left, intv.open_right, intv.closed_left, intv.closed_right"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Intervals can also be sorted (in lexicographical order):"
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[Interval(0, 10, closed='right'), Interval(10, 20, closed='right')]"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sorted([pd.Interval(10, 20), pd.Interval(0, 10)])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You cannot create an Interval with the left side greater than the right side:"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {
"collapsed": false
},
"outputs": [
{
"ename": "ValueError",
"evalue": "left side of interval must be <= right side",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-37-89db02665b32>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mInterval\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m10\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m/Users/shoyer/dev/pandas/pandas/src/interval.pyx\u001b[0m in \u001b[0;36mpandas.lib.Interval.__init__ (pandas/lib.c:45402)\u001b[0;34m()\u001b[0m\n\u001b[1;32m 55\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"invalid option for 'closed': %s\"\u001b[0m \u001b[0;34m%\u001b[0m \u001b[0mclosed\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 56\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mleft\u001b[0m \u001b[0;34m<=\u001b[0m \u001b[0mright\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 57\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'left side of interval must be <= right side'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 58\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mleft\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mleft\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 59\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mright\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mright\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mValueError\u001b[0m: left side of interval must be <= right side"
]
}
],
"source": [
"pd.Interval(10, 0)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## IntervalIndex\n",
"\n",
"`IntervalIndex` is a specialized `Index` subclass for interval data."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Creating an `IntervalIndex`\n",
"\n",
"There are three ways to construct an IntervalIndex:"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"IntervalIndex(left=[0, 1, 2],\n",
" right=[1, 2, 3],\n",
" closed='left')"
]
},
"execution_count": 30,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# using the constructor\n",
"pd.IntervalIndex(left=[0, 1, 2], right=[1, 2, 3], closed='left')"
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"IntervalIndex(left=[0, 1, 2],\n",
" right=[1, 2, 3],\n",
" closed='right')"
]
},
"execution_count": 31,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# from a sequence of Interval objects\n",
"pd.Index([pd.Interval(0, 1), pd.Interval(1, 2), pd.Interval(2, 3)])"
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"IntervalIndex(left=[0, 1, 2],\n",
" right=[1, 2, 3],\n",
" closed='right')"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# from breaks\n",
"pd.IntervalIndex.from_breaks([0, 1, 2, 3])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### `IntervalIndex` has all the properties of `Interval`"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"index = pd.IntervalIndex.from_breaks([0, 1, 2, 3])"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"Int64Index([0, 1, 2], dtype='int64')"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"index.left"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"Int64Index([1, 2, 3], dtype='int64')"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"index.right"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"'right'"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"index.closed"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Including the derived ones:"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"Float64Index([0.5, 1.5, 2.5], dtype='float64')"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"index.mid"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"False"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"index.closed_left"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"index.closed_right"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Pandas objects with an `IntervalIndex` index"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"s = pd.Series(list('abc'), index)"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"(0, 1] a\n",
"(1, 2] b\n",
"(2, 3] c\n",
"dtype: object"
]
},
"execution_count": 40,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"s"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Indexing selects all overlapping values:"
]
},
{
"cell_type": "code",
"execution_count": 41,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"'a'"
]
},
"execution_count": 41,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"s.loc[0.5]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Slicing also works, as long as the intervals are sorted and non-overlapping:"
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"(0, 1] a\n",
"(1, 2] b\n",
"dtype: object"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"s.loc[0:2]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that while `.loc` maintains the original `Intervalindex`, `.reindex` will set a new index:"
]
},
{
"cell_type": "code",
"execution_count": 45,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"(0, 1] a\n",
"(1, 2] b\n",
"(2, 3] c\n",
"dtype: object"
]
},
"execution_count": 45,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"s.loc[[0.5, 1.5, 2.5]]"
]
},
{
"cell_type": "code",
"execution_count": 47,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"0.5 a\n",
"1.5 b\n",
"2.5 c\n",
"dtype: object"
]
},
"execution_count": 47,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"s.reindex([0.5, 1.5, 2.5])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### An IntervalTree allows for efficient indexing of non-sorted or overlapping intervals\n",
"\n",
"From an API perspective, you can use an IntervalIndex in the same way:"
]
},
{
"cell_type": "code",
"execution_count": 366,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"index2 = pd.IntervalIndex(left=[0, 1, 2], right=[10, 5, 3], closed='left')\n",
"s2 = pd.Series(list('abc'), index2)"
]
},
{
"cell_type": "code",
"execution_count": 367,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[0, 10) a\n",
"[1, 5) b\n",
"[2, 3) c\n",
"dtype: object"
]
},
"execution_count": 367,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"s2"
]
},
{
"cell_type": "code",
"execution_count": 370,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"[0, 10) a\n",
"[1, 5) b\n",
"dtype: object"
]
},
"execution_count": 370,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"s2.loc[1.5]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Benchmarking code (skip this section)"
]
},
{
"cell_type": "code",
"execution_count": 281,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import timeit\n",
"import textwrap\n",
"import math\n",
"import seaborn as sns\n",
"\n",
"\n",
"def magic_timeit(stmt, setup, ncalls=None, repeat=3, force_ms=False):\n",
" \"\"\"Time execution of a Python statement or expression\n",
" Usage:\\\\\n",
" %timeit [-n<N> -r<R> [-t|-c]] statement\n",
" Time execution of a Python statement or expression using the timeit\n",
" module.\n",
" Options:\n",
" -n<N>: execute the given statement <N> times in a loop. If this value\n",
" is not given, a fitting value is chosen.\n",
" -r<R>: repeat the loop iteration <R> times and take the best result.\n",
" Default: 3\n",
" -t: use time.time to measure the time, which is the default on Unix.\n",
" This function measures wall time.\n",
" -c: use time.clock to measure the time, which is the default on\n",
" Windows and measures wall time. On Unix, resource.getrusage is used\n",
" instead and returns the CPU user time.\n",
" -p<P>: use a precision of <P> digits to display the timing result.\n",
" Default: 3\n",
" Examples:\n",
" In [1]: %timeit pass\n",
" 10000000 loops, best of 3: 53.3 ns per loop\n",
" In [2]: u = None\n",
" In [3]: %timeit u is None\n",
" 10000000 loops, best of 3: 184 ns per loop\n",
" In [4]: %timeit -r 4 u == None\n",
" 1000000 loops, best of 4: 242 ns per loop\n",
" In [5]: import time\n",
" In [6]: %timeit -n1 time.sleep(2)\n",
" 1 loops, best of 3: 2 s per loop\n",
" The times reported by %timeit will be slightly higher than those\n",
" reported by the timeit.py script when variables are accessed. This is\n",
" due to the fact that %timeit executes the statement in the namespace\n",
" of the shell, compared with timeit.py, which uses a single setup\n",
" statement to import function or create variables. Generally, the bias\n",
" does not matter as long as results from timeit.py are not mixed with\n",
" those from %timeit.\"\"\"\n",
"\n",
" units = [\"s\", \"ms\", 'us', \"ns\"]\n",
" scaling = [1, 1e3, 1e6, 1e9]\n",
"\n",
" timer = timeit.Timer(stmt, setup)\n",
"\n",
" if ncalls is None:\n",
" # determine number so that 0.2 <= total time < 2.0\n",
" number = 1\n",
" for _ in range(1, 10):\n",
" if timer.timeit(number) >= 0.1:\n",
" break\n",
" number *= 10\n",
" else:\n",
" number = ncalls\n",
"\n",
" best = min(timer.repeat(repeat, number)) / number\n",
"\n",
" if force_ms:\n",
" order = 1\n",
" else:\n",
" if best > 0.0 and best < 1000.0:\n",
" order = min(-int(math.floor(math.log10(best)) // 3), 3)\n",
" elif best >= 1000.0:\n",
" order = 0\n",
" else:\n",
" order = 3\n",
"\n",
" return {'loops': number,\n",
" 'repeat': repeat,\n",
" 'timing': best * scaling[order],\n",
" 'units': units[order]}\n",
"\n",
"\n",
"def time_interval_tree(build_tree, query_tree, size, overlap, dtype='int'):\n",
" setup_generic = textwrap.dedent(\"\"\"\n",
" import pandas as pd\n",
" import numpy as np\n",
" import banyan\n",
" import intervaltree as it\n",
" import bx.intervals.intersection\n",
" \n",
" rs = np.random.RandomState(0)\n",
" left = (rs.rand({size}) * 1e6).astype({dtype})\n",
" right = left + ({overlap} * rs.rand({size}) * 1e6).astype({dtype}) + 1\n",
" intervals = [((xi, yi), n) for n, (xi, yi) in enumerate(zip(left, right))]\n",
" \"\"\").format(size=size, overlap=overlap, dtype=dtype)\n",
"\n",
" construct_time = 1e3 * timeit.timeit(build_tree, setup_generic, number=1)\n",
"\n",
" setup_tree = setup_generic + textwrap.dedent(build_tree.format(dtype=dtype))\n",
" guarded_query = textwrap.dedent(\"\"\"\n",
" try:\n",
" {query}\n",
" except KeyError:\n",
" pass\n",
" \"\"\").format(query=query_tree)\n",
" points = (np.linspace(1e-3, 1 - 1e-3, num=10) * 1e6).astype(dtype)\n",
" query_time = np.mean([\n",
" magic_timeit(guarded_query.format(point=p), setup_tree, force_ms=True)['timing']\n",
" for p in points])\n",
"\n",
" return {'construct': construct_time, 'query': query_time}\n"
]
},
{
"cell_type": "code",
"execution_count": 179,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"bench_code = {\n",
" 'pandas': {\n",
" 'setup': \"tree = pd.core.interval.IntervalTree(left, right)\",\n",
" 'query': 'tree.get_loc({point})'\n",
" },\n",
" 'banyan': {\n",
" 'setup': \"tree = banyan.FrozenSortedDict(intervals, key_type=({dtype}, {dtype}), updator=banyan.OverlappingIntervalsUpdator)\",\n",
" 'query': 'tree.overlap_point({point})'\n",
" },\n",
" 'intervaltree': {\n",
" 'setup': \"tree = it.IntervalTree([it.Interval(xi, yi, n) for n, (xi, yi) in enumerate(zip(left, right))])\",\n",
" 'query': 'tree[{point}]' \n",
" },\n",
" 'bx': {\n",
" 'setup': (\"tree = bx.intervals.intersection.Intersecter()\\n\"\n",
" \"for start, end in zip(left, right):\\n\"\n",
" \"\\ttree.add_interval(bx.intervals.intersection.Interval(start, end))\"),\n",
" 'query': 'tree.find({point}, {point})'\n",
" }\n",
"} \n"
]
},
{
"cell_type": "code",
"execution_count": 174,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 35min 55s, sys: 1min 57s, total: 37min 52s\n",
"Wall time: 38min 19s\n"
]
}
],
"source": [
"%%time\n",
"bench_results_int = {k: {o: time_interval_tree(v['setup'], v['query'],\n",
" 100000, overlap=o, dtype='int')\n",
" for o in [0.1, 0.01, 0.001, 1e-4, 1e-5]}\n",
" for k, v in bench_code.items()}"
]
},
{
"cell_type": "code",
"execution_count": 168,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 35min 13s, sys: 1min 30s, total: 36min 44s\n",
"Wall time: 36min 56s\n"
]
}
],
"source": [
"%%time\n",
"# bx does not support floats\n",
"bench_results_float = {k: {o: time_interval_tree(v['setup'], v['query'],\n",
" 100000, overlap=o, dtype='float')\n",
" for o in [0.1, 0.01, 0.001, 1e-4, 1e-5]}\n",
" for k, v in bench_code.items() if k != 'bx'}"
]
},
{
"cell_type": "code",
"execution_count": 309,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"df = pd.concat([pd.Panel(bench_results_int).to_frame(),\n",
" pd.Panel(bench_results_float).to_frame()],\n",
" keys=['int', 'float'])\n",
"df.columns.name = 'package'\n",
"df.index.names = ['dtype', 'method', 'Interval density']\n",
"df = df.swaplevel(0, 1)\n",
"df = df.sort_index()\n",
"\n",
"s = df.stack()\n",
"s.name = 'Time (ms)'"
]
},
{
"cell_type": "code",
"execution_count": 311,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th></th>\n",
" <th>package</th>\n",
" <th>banyan</th>\n",
" <th>bx</th>\n",
" <th>intervaltree</th>\n",
" <th>pandas</th>\n",
" </tr>\n",
" <tr>\n",
" <th>method</th>\n",
" <th>dtype</th>\n",
" <th>Interval density</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th rowspan=\"10\" valign=\"top\">construct</th>\n",
" <th rowspan=\"5\" valign=\"top\">float</th>\n",
" <th>0.00001</th>\n",
" <td>33.795834</td>\n",
" <td>NaN</td>\n",
" <td>6505.264044</td>\n",
" <td>182.418108</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.00010</th>\n",
" <td>35.412073</td>\n",
" <td>NaN</td>\n",
" <td>5485.455990</td>\n",
" <td>209.792137</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.00100</th>\n",
" <td>33.347130</td>\n",
" <td>NaN</td>\n",
" <td>4815.956831</td>\n",
" <td>204.849005</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.01000</th>\n",
" <td>33.624172</td>\n",
" <td>NaN</td>\n",
" <td>4272.170067</td>\n",
" <td>99.512100</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.10000</th>\n",
" <td>49.762011</td>\n",
" <td>NaN</td>\n",
" <td>3546.745062</td>\n",
" <td>152.156115</td>\n",
" </tr>\n",
" <tr>\n",
" <th rowspan=\"5\" valign=\"top\">int</th>\n",
" <th>0.00001</th>\n",
" <td>38.027048</td>\n",
" <td>484.008074</td>\n",
" <td>6560.597181</td>\n",
" <td>197.359800</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.00010</th>\n",
" <td>40.066957</td>\n",
" <td>551.862001</td>\n",
" <td>5488.254070</td>\n",
" <td>183.032036</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.00100</th>\n",
" <td>55.298090</td>\n",
" <td>396.842957</td>\n",
" <td>5040.079832</td>\n",
" <td>200.254917</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.01000</th>\n",
" <td>43.848038</td>\n",
" <td>385.418892</td>\n",
" <td>4316.785097</td>\n",
" <td>77.873945</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.10000</th>\n",
" <td>59.370995</td>\n",
" <td>359.165907</td>\n",
" <td>3899.135828</td>\n",
" <td>80.479145</td>\n",
" </tr>\n",
" <tr>\n",
" <th rowspan=\"10\" valign=\"top\">query</th>\n",
" <th rowspan=\"5\" valign=\"top\">float</th>\n",
" <th>0.00001</th>\n",
" <td>0.337233</td>\n",
" <td>NaN</td>\n",
" <td>0.039368</td>\n",
" <td>0.010840</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.00010</th>\n",
" <td>0.369326</td>\n",
" <td>NaN</td>\n",
" <td>0.065924</td>\n",
" <td>0.010664</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.00100</th>\n",
" <td>0.351634</td>\n",
" <td>NaN</td>\n",
" <td>0.259455</td>\n",
" <td>0.012111</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.01000</th>\n",
" <td>0.402356</td>\n",
" <td>NaN</td>\n",
" <td>2.010541</td>\n",
" <td>0.016342</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.10000</th>\n",
" <td>0.691712</td>\n",
" <td>NaN</td>\n",
" <td>14.636299</td>\n",
" <td>0.045705</td>\n",
" </tr>\n",
" <tr>\n",
" <th rowspan=\"5\" valign=\"top\">int</th>\n",
" <th>0.00001</th>\n",
" <td>0.379258</td>\n",
" <td>0.000447</td>\n",
" <td>0.042750</td>\n",
" <td>0.011488</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.00010</th>\n",
" <td>0.400268</td>\n",
" <td>0.000689</td>\n",
" <td>0.078484</td>\n",
" <td>0.011800</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.00100</th>\n",
" <td>0.394427</td>\n",
" <td>0.002353</td>\n",
" <td>0.318718</td>\n",
" <td>0.013086</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.01000</th>\n",
" <td>0.411563</td>\n",
" <td>0.025435</td>\n",
" <td>2.490402</td>\n",
" <td>0.017582</td>\n",
" </tr>\n",
" <tr>\n",
" <th>0.10000</th>\n",
" <td>0.737902</td>\n",
" <td>0.332723</td>\n",
" <td>18.203829</td>\n",
" <td>0.046902</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
"package banyan bx intervaltree \\\n",
"method dtype Interval density \n",
"construct float 0.00001 33.795834 NaN 6505.264044 \n",
" 0.00010 35.412073 NaN 5485.455990 \n",
" 0.00100 33.347130 NaN 4815.956831 \n",
" 0.01000 33.624172 NaN 4272.170067 \n",
" 0.10000 49.762011 NaN 3546.745062 \n",
" int 0.00001 38.027048 484.008074 6560.597181 \n",
" 0.00010 40.066957 551.862001 5488.254070 \n",
" 0.00100 55.298090 396.842957 5040.079832 \n",
" 0.01000 43.848038 385.418892 4316.785097 \n",
" 0.10000 59.370995 359.165907 3899.135828 \n",
"query float 0.00001 0.337233 NaN 0.039368 \n",
" 0.00010 0.369326 NaN 0.065924 \n",
" 0.00100 0.351634 NaN 0.259455 \n",
" 0.01000 0.402356 NaN 2.010541 \n",
" 0.10000 0.691712 NaN 14.636299 \n",
" int 0.00001 0.379258 0.000447 0.042750 \n",
" 0.00010 0.400268 0.000689 0.078484 \n",
" 0.00100 0.394427 0.002353 0.318718 \n",
" 0.01000 0.411563 0.025435 2.490402 \n",
" 0.10000 0.737902 0.332723 18.203829 \n",
"\n",
"package pandas \n",
"method dtype Interval density \n",
"construct float 0.00001 182.418108 \n",
" 0.00010 209.792137 \n",
" 0.00100 204.849005 \n",
" 0.01000 99.512100 \n",
" 0.10000 152.156115 \n",
" int 0.00001 197.359800 \n",
" 0.00010 183.032036 \n",
" 0.00100 200.254917 \n",
" 0.01000 77.873945 \n",
" 0.10000 80.479145 \n",
"query float 0.00001 0.010840 \n",
" 0.00010 0.010664 \n",
" 0.00100 0.012111 \n",
" 0.01000 0.016342 \n",
" 0.10000 0.045705 \n",
" int 0.00001 0.011488 \n",
" 0.00010 0.011800 \n",
" 0.00100 0.013086 \n",
" 0.01000 0.017582 \n",
" 0.10000 0.046902 "
]
},
"execution_count": 311,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Comparison to other interval indexing code"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Is the pandas `IntervalIndex` fast? To understand its performance, we benchmark it against three other open source interval indexes implementations in Python: [Banyan](https://pythonhosted.org/Banyan/), [bx-python](https://bitbucket.org/james_taylor/bx-python/wiki/Home) and [intervaltree](https://github.com/chaimleib/intervaltree).\n",
"\n",
"Regardless of performance, not all these libraries support the same features. We wrote a new implementation of an interval tree in Cython for pandas because we wanted fast construction from numpy arrays and fast querying for both integer and float data, and for intervals closed on each possible side (left, right, both or neither). Currently, pandas only support point overlap queries, but it would be easy to extend the pandas IntervalTree to other types of queries, too. However, one important difference is that the pandas IntervalTree does not support mutation.\n",
"\n",
"In theory, all of these interval indexes support construction in time $O(n \\log n)$ and querying in time $O(\\log n)$, where $n$ is the number of intervals. However, in practice their performance varies by orders of magnitude.\n",
"\n",
"In this benchmark, we construct and query 100,000 randomly placed intervals with `int` and `float` dtypes (bx-python only supports integers). The left side of each interval is located uniformly at random between between 0 and 1e6. The right side of each interval is offset some distance uniformly at random between 0 and $10^6 \\rho$, where $\\rho$ is a constant corresponding to interval density. The benchmark code for each of these is included above.\n",
"\n",
"##### Interval density\n",
"\n",
"Varying interval density helps us comprehensively understand interval tree performance:"
]
},
{
"cell_type": "code",
"execution_count": 362,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def make_intervals(density, size=20):\n",
" rs = np.random.RandomState()\n",
" left = rs.rand(size) * 1e3\n",
" right = left + density * rs.rand(size) * 1e3 + 1\n",
" return left, right\n",
"\n",
"def plot_intervals(density, size=20):\n",
" plt.figure(figsize=(5, 1))\n",
" for n, (l, r) in enumerate(sorted(zip(*make_intervals(density, size)))):\n",
" plt.plot([l, r], [n, n], 'k')\n",
" plt.axis('off')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Intervals sampled with low density rarely overlap:"
]
},
{
"cell_type": "code",
"execution_count": 363,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAUMAAABcCAYAAAD0xNf9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAAetJREFUeJzt3ctRAkEARdHblikailEQikmZybhFFvJxYHrgnCp3Vsnq\nVV8ah7EsSwCv7m3rFwAwA2MIkDEEqIwhQGUMASpjCFAZQ4DKGAJUxhCgMoYAlTEEqIwhQGUMAap6\n3/oFAFSNMQ7VxwW/+rUsy+faf9/JEKAanmcIIJOBlVyRucfukry3kMkAyWTgyJnT3TSnuHtwMgTI\nyRCgcoECu3LDJcVTp+2aZDJAMhmgkslwkytzVarugEwGSCYDVDKZJ3RhwkpXfpHJAMlkgMrJkMmN\nMQ5jjO+Tn8PWr4vnYwwBksk8wNaPc4dLOBkC5GQIUPmcISvx72nsnUwGSCYDVDKZf7jx29BKJjMh\nmQyQTAaoZDJHfDiaVyaTAXIyfHouOeAyToYAuUABqJwMd+PkuX6e5wcrM4YAyWSAym3yNHyjG2xL\nJgMkkwEqmTyFM4ksjeEBZDJAMhmgksmb8YQYmItMBsjJ8CFckMD8nAwBcoECUMnksyQuvAaZDJBM\nBqh2nMk+pwesSSYDJJMBqkky+Y/klbjAQzgZAuQ9Q4DKGAJUxhCgMoYAlTEEqIwhQGUMASpjCFAZ\nQ4DKGAJUxhCgMoYAlTEEqIwhQGUMASpjCFAZQ4DKGAJUxhCgMoYAlTEEqIwhQGUMAar6AaZvmxee\n6B5sAAAAAElFTkSuQmCC\n",
"text/plain": [
"<matplotlib.figure.Figure at 0x22bf60510>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"plot_intervals(0.05)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In contrast, intervals sampled with high density usually overlap:"
]
},
{
"cell_type": "code",
"execution_count": 365,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAUMAAABcCAYAAAD0xNf9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAAflJREFUeJzt3Mtt3EAURcFDwSkqFEehUBQkvfHCO48kfprdVRFwdfHO\nNDDbvu8BrO7t7g8AGIExBMgYAlTGEKAyhgCVMQSojCFAZQwBKmMIUBlDgMoYAlTGEKAyhgBV/br7\nA2BE27Z9VO93f8eDfO77/vvuj/gJlyFAtfk/QwCZzOIWyeHHJ+wVZDJAMpkFLXINnmHqC9NlCJDL\nEKDygMICFs7iqbP2aDIZIJkMUMlkJnJwDkvMxchkgGQyQCWTeaCFX4dLvp9GJgMkkwEqmczABsth\neTo5mQyQyxBe9b5t25VXqkv0Yi5DgDygAFQymQMN9uBxBSk7EZkMkEwGqGQy/1gwc48ilycgkwGS\nyQCVTKZh8lhqciuZDJBMfpRBLrg7uBo5ncsQIJchQOUB5XInp66chG+SyQDJZIBKJh9i4Vfer5Lx\nDEsmAySTASqZ/JKJM1i2wl8yGSCZDFAtnskT5+9ZZDXTkskALZLJLsBTuRaZgssQoEUuQ4D/GfoB\nZbC8lYMwMZkMkEwGqAbK5MGS+KskNDycTAZIJgNUN2TyhTksXYGXuQwB8pshQGUMASpjCFAZQ4DK\nGAJUxhCgMoYAlTEEqIwhQGUMASpjCFAZQ4DKGAJUxhCgMoYAlTEEqIwhQGUMASpjCFAZQ4DKGAJU\nxhCgMoYAVf0BRoygVbOMkW8AAAAASUVORK5CYII=\n",
"text/plain": [
"<matplotlib.figure.Figure at 0x22c88fb90>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"plot_intervals(0.5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Different interval indexing strategies turn out to work better for different typical densities."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Construction"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We see that pandas constructs interval trees faster than bx-python and intervaltree, but more slowerly than banyan:"
]
},
{
"cell_type": "code",
"execution_count": 320,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAa0AAAEgCAYAAAAHeCwxAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs3Xd8W/W9//HX0fQeiZ3lxJnwgbBnwiqrpYu20HVbSim0\nwO2g+0LLbQvd0PnrvbS9vaV00F5ooYUOWuigjBJ22OsLSchySGI7sRMvzfP743skS5Zky4ltSc7n\n+XgYSUdfHX11Is5b3+/5nu9xXNdFKaWUqgS+UldAKaWUKpaGllJKqYqhoaWUUqpiaGgppZSqGBpa\nSimlKoaGllJKqYqhoaXGTUT+R0Q+P0Xv9RcRee8UvVeriDwvImHv8d0i8oGpeO9iichuEVk0ztec\nLyL/mqQq7RERuUREri51PVTlCZS6AtONiJwDfAoQYDfwBPA1Y8yqSXq/U4BfGmMWTNL6zwc+YIw5\nKbXMGPOhSXqvLwJLjTHpkDLGvGEy3quAzwI/M8ZEvMeu91c2jDH1U/l++f79ReTnwCZjzBf2YtXX\nAmtE5DvGmM69q6Xal2hLawKJyKeA/wd8FZgFLAB+ALy5xPXyl/L9K4HXujoP+FWp6wIgItP2B6WI\n+L0fBrdjt7lSRXN0RoyJISKNwGbgfGPM7wqUCQPfAN7hLboJ+IwxJuq1mH4FfBf4DJAA/tMY83Pv\ntW8AvoUNwl1euR8B3UAIGMC2CgT4d+BgYBAbmJ8CTiLj1/HIFpqILAD+CzgR+2PmRmzgPoFtkQ8C\nMWPMjJG/tEXkIuAyYAZwH/BBY8wr3nNJ4EPAp4FW4P+MMZfk2TavA/4AOEAEWGOMOUJE7vbqeZ33\nq/8i4CHgAu+zn+d95i8BYeBSY8z1Gdv7a972DgO3Ap80xgzlef9XAdcZY/bLWHaX994/FREH+Bxw\nIVAN3AF81BizS0R+ATxpjPmuiLQBm4BLjDE/FJGlwMPGmJneOs/E/qhZCDznbaunvefWAz8EzgX2\nA2qNMckR9UwCy4wx6/J8J/6fMeY7eT7b+V69HwfeC7wCfMQY80/v+Ubs9+n1QBL4GXClt10fA4LY\nf/849t/5B9jvWhT4pzHmLSIyD7gG+z3r8+pyjbf+L5L9ffykt03PAS40xpw2ss5KFaItrYlzHFCF\n3TEW8jngWOAw7+9YIPPY0GygAZgHfAD4gbdDAbgOuNgY0wAcBNxljBkAXgdsMcbUG2MaUmGB3Tnc\nbIxpBP6PUbq6vJbYbcDL2J1pG3CjMeYFbAA+4K1/hveS9LpE5DTg69hgmAtsAH494i3eCBwNHAq8\nU0ReO7IOxpg7vPX82nuvI0a+l+dY4ElsQN6IDf4jgaXYnf33RaTGK3s1sAy7rZd5n+uKfNsAOAQw\nBZ4DG5LvA04BlgB1wPe95+72lgOcDKwDXpXx+F4AETkC++94kVf//wX+KCLBjPd5FzY8mkYGVh4j\nvxP/HKXsCmANMBMbSLeISJP33M+xAbQUOAI4AxsmzwMfZPjfv9kYcy32+/QNb9lbRMQH/AkbivOA\n04FPiMgZGe+f+X28wVv2AvbfRqmiaWhNnJlA1xg7mnOALxtjuowxXdjWQeYgg5j3fMIYczv2F6t4\nz0WBg0SkwRjTa4x53FvuFHiv+40xfwTIaFkUKnssNnAuNcYMGmMixpj7x3hNynuwLZQnjDFR4HLg\nOBFpzyhztTFmlzFmE3AXcHiBdTlFvN/LxphfGGNcbGDNw26zmDHm79jttMxrGV0EfMoY02OM6QOu\nwoZCPk3YY5Cjfc7vGGPWG2P6vc/5Lm+HfS9woveeJwHfBE7wXncycI93/2Lgf40xjxhjXK9FGAFW\nes+7wH8bYzoyjquNptB3Ip/txpj/8r5bN2ED+kwRmY0NyU96//adwPcY3k6F/j0ylx8DtBhjvmqM\niRtjXgZ+Qva2zvd93A00otQ4aGhNnG6gxduJFTIP2xJJ2egtS69jROgNYH/RA7wNeAOw3hvVtpLR\nbS6u2oDtXtpQxC/7fFKtKwC8HXo3tlWTsjXjfuZn2hPbMu4Peu/ZOWJZHbYrsgZYLSI7RWQn9hhK\nS4H17gBGG+SQ9Tmx/3YBYLYxZi3Qjw3jk7Ct1i0isj+2xZUKrYXAp1P18eo0n+zvwKZR6jDSeL4T\nHSMeb/Detx3b/fdKRp1+hN1+xVoIzBvxuS7HHtdNyfd9rAd6x/E+SunowQn0APZX89lA3mNawBZg\nEfC897jdWzYmY8yjwFleV95Hsa2MdvJ3+eXrCuzH7sRT5mTc3wS0ewfIE3nWNZrUZwJARGqxrc6R\nO8li7EloFtKFDbDlGV2mo3kK+OQoz2d9Tuy2jzMcovdgu0iDxpgtInIPcD7QjD0uCDbovmaM+foo\n71P0QeZRvhP5tI14vBB7DHET9ns7s8CPlkLfr0wbsS3g/Qu8d6Gu6QMZ3jZKFaWsWloiMltEHil1\nPfaEMaYXe7zkByLyFhGpEZGgiLxeRL7hFbsR+LyItIhIi1f+l2Ot21vPe0Sk0QuV3diBGmB3mjNF\npCHjJfm6dJ4A3iAizSIyB/hExnMPYw/OX+3Vu0pEjs9Y//wRx10yu/FuBC4QkcO8gQ9fBx40xmws\n8HFG6/7bBizyutn2ircDvhb4noi0AohI24jjLJkeAZq8AQX53Ah8UkQWiUgdw8ffUjv6e4BL8I5f\nYY9zXQL8y+vKxKvPB0XkWBFxRKRWRN7orW9cxvhO5DNLRD7mve4dwAHAX4wxW4G/Ad8VkXoR8YnI\nUm9gCuT/99+GPa6X8jCwW0QuE5FqEfGLyMEicrT3fKF/z5OxrV+lilY2oeXtqC4F1pe4KnvMGPNd\n7Ei9zwPbsb9AP8zw4IyvAo9if9U/5d3/asYqRvuVfS7wsoj0Yo+NvMd7zxewO9R1IrJDROaS/5ft\nL7EDGNZjR779OlXG2+m9CTtYYSP21/c7vdfdCTwLbBWR7Rn1TL32TuAL2NblFmAx2ccyRtZjtHOf\nbvZuu0Xk0TzP53vtaNvsM9jBBw962+3vQN7WgHc87ufY7ZzPT7Hb8F7sQIsBbOsm5V5st2QqtFZh\nRxmmHmOMWY09zvZ9bHfkS9jRj+MZwptZNu93osBrHsSOSOwEvgK8zRiz03v+POwI1Oe8et3McEs8\n37//dcByryvwFi+4z8R2j67z3uPH2EFFqffP+owiUoU9lvaLcXx2pcpnyLuIfAjbxfZpk3FyqVJT\nxWv9/gs4vMiBEGoPicglwHxjzGdLXRdVWaYktERkBXYE2aneQIUfYoc/R7BDa9eKyO+wrZPTsOcn\nFToupJRSah816d2DInIZti8/7C06CwgZY47HTpvzHQBjzNuMnR7oIQ0spZRS+UzFMa01wFsZPhh7\nIvaYCsaYh7AnnaYZY3RaF6WUUnlN+pB3Y8wtkj0rdT12ypmUhIj4xnuOkOu6ruPs9SAzpZQaSXcs\nZawU52ntIvskznEHFoDjOHR2jjaBwb6ptbVet8sIuk3y0+2SX2vrlE6kr8apFEPeV2HP4sc7g/+p\nEtRBKaVUBZrKllZqmOKtwGtEJHV9qQumsA5KKaUq2JSEljFmPXC8d9/FXqpCKaWUGpeymRFDKaWU\nGouGllJKqYqhoaWUUqpiaGgppZSqGBpaSimlKoaGllJKqYpRsaGVTE7kRW6VUkpVgooNrXf96R0a\nXEoptY8pxdyDEyKSjHDx/e9heeOhNIebaQ7PYGa4lZbwLBpCDTSEGqkN1OFzKjaXlVJKjVCxoZXy\nXG/hqQsdHGoDddQF6okmI9QEaqkL1tEQbKI5NIPm8Ezm1bTRGGqiLlhPXbAev+OfwtorpZQaj4oN\nraAT5K0L38XO2A56ozvpjfYylBjk4ObD2B3bxe7Ybu92F7ujvfQn+tkR7R5zvamQG0oMUhOopTZQ\nS0OokcZQMzPCM1hYt4T6YAP1wQbqAvUEfBW7CZVSquJU7B73pjf/ju7u/qLKxpNx1u5+ia6h7XRH\nutgZ6aY32kMkEWH/xgPZHetNh1xffBe90V764330xnrGXHeN3wbbYGKImkA1NYFa6oMNNAQbmRlu\nYWnD/tQH66kPNlIfbNCQU0qpvVCxe1Cfr/hjVQFfAGk8EGk8sKjy8WScLQOb6RraTlekkx2Rbnqi\nO4kmoiyuX8IurwXX57XoeqI76Y/30RffNea6q/3V1ATqGEwMUOWvosZfS12w3oZcVQvSeCB1XujV\nB+sJ+kJFf85kMqmDU5RS05rjuu7YpcqTWy4XsEu4CbqHOuka6qRzaBs7It3sjO4klozSVjOfXRkB\ntzu2i57oTvrixdU97AXbYGKAsC9MdaCGukAd9cEGZla1srzpENtV6a/jyscvw3Ecrln503GF+nSn\nFzvMT7dLfq2t9Xrl4jKmoVUCSTdJf6yPzsh2tg9to3uok57IDmJulNaqOVndlbtju+iJ7GB3kSGX\n4uDgd/z2zxck6ASoDtTSVtNGyBcm5A8T8oUI+8P4HT9J16UuWEuNv46wv4qQP+SVCxH2hdP3Q74w\nQV+wokZl6s45P90u+WlolbeK7R6sZD7HR32ogfpQA0vqlxX1mmgiSnekk+2D2+ga2s6OaBfRZIzm\n0Ax6Iz38c+tfcRn+AeLiEnfjxN04JCMA9MZ62DrYMSGfwe/4CTpBgv6gDTRfmCp/NTWBGkK+UDoU\nM8Muc3nYX5X1fDhPuYkYyem6LhX8w0wpNYK2tKaJZDLJJQ+8H8ch3T3oui7RZITB+AD98X7ibpwZ\n4ZlEExEiySjRZIRoIsqWgc081LmKocQgkcQQkUSEaDJKY6iJQ5oPT5eLJu3rdka66RjYNOmfye/4\nCfnChP3hnNDLDMPwiLALe/eDvhB/2HAzwUCAC5Z92J7WELAtScfRH9Pa0spPW1rlTUNrGkkmk7S2\n1hc9qnJP9UR28HzvswzE+xmIDzCYGGAgPsCc6rmcPu912SGXiPDMzie5ZcOvc9Yzp3oex7QcZ8sn\nI0S813QNdbKx72Ucx4cPu/+wrUgHlyQJN7FX9fc7fnvOXqCe2mAddYE6agJ11AbsoJjaQK099SF9\n35YL+8LTKuw0tPLT0CpvGlrTTDnuiOLJOP3xPgYTgwzE+xn0gq4uUM8BTQfllH9251P8dv0NXtlB\nBhMDuLgc3bKSDx7wceLJOLFklKjXWny861FuWv+rrHU4OFndpT58HDPzOAaS/eyI7KAn0k3MjRFN\nRov+HAEnQG2gjtqgDbXaQH1O0NV691OBWBuoJVSmYVeO35VyoKFV3jS0ppnpuCNKukkiiSGSJKkN\n1OU8v7FvPQ91rsoKxYF4Pzsi3elz7ZbULePyw76M4zjcv+1efvrS/+Ss5+Cmw3h12+voj/V7pzDs\npj/Wz8b+9WzoexmwLb6kmxhXa89PgLpQnXfiel1WuNkWnbcsUOcFnT3BPeQv/nSHPTEdvysTQUOr\nvGloTTO6Ixrmui5XPXkFgaCfS5dfmW7t7I7tYuvAFgYTAwwmBhmKDzKYGGRO9VwOn3l0znoe7ryf\n36z7JYOJQaLeoBaAla0n8ab2t9If76M/1ucFXR/P73yaJ3c+lrOegBMg4SayWoCjCTgBrxVXN6K7\ncjjcMkOvzltezLl9U9WVXIk0tMqbhtY0o6GVzXVdWlvr6erqm5D1JdwEQ17Q+X0BmkLNOWXW7HqR\nRzofGBGKAxw+82jeMP8tDMS9lpwXdA933s+DnfflrKc+0EBVoIr+WD8DieLDJeQLZbXcwr4QASdI\nfaiBhmATDYFGfvPy9TiOw6WHXEFNoIawv8r7m5hRm5VMQ6u8aWhNMxpaucp9m/RGe9g+uJXBhA23\nVMtvQe1CDmo+FLBhacOun1Vb7+bebXcSSUTsKQ2eOdVzmRluTXdr2uOIA+OuT8AJEHCChP3DpzFU\ne8FW5bPBFvZXUZUOuuzlqeeq/NV2ma+qoqYv09Aqbxpa00y576BLYTpvk8yWX9AXoiHUmPV8PBnn\nqR2P81j3w+nW3ct9a9LP+/Bx/KyTibnR9CkPWwe30hPdMaH1DDiBjECzQTYy+MK+cPrxyNuwL7Os\nLRdwghM+wMV1XS5adY7vD2ffVrE7xumucn7+KKVy+B1/uiswn4AvwJEtx3BkyzHpZfnO6cu0bvca\nXux9nt2xXeyK9drbaC8rZ53EUTOPJZIcYigx5J3TN8S/tt3Fkzuyj+EFnSBttQtoDs1gKGnP/UuV\n74n0EEkO7fWpCz58I4IvnA63rFZgRvClAi+7hZh6bZjvPvN1gFXA8XtVOTVpNLSU2sf4fD6+f9xP\nCw7EWFK/rOiZWgCq/DUsqktNJN3LrqidfuzUuWdwwuyTc8rfuPYX3PnKHenHfidAbaCWk+ecjjQe\nRCRpwy0VjN1DnfR73ZyumySWjDKU8XwkOUR/vI8dke6sgTJ74biJWImaHBpaSu2DfD7fhE2qfEDT\ncg5oWl50+YOaDyXkD3ktuF3eXJu7mFM9L+96fv7Sj3lg+73px9X+GhqCDbx10bs4qmVFVtmkm6Sj\nfyP9iQFCviB+fESSsXS4DcUHGUoOt/qG0reDPLvzaYaSg3u+IdSU0NBSSk2pQ2ccwaEzjii6/MHN\nh1HlD9uWXHS4yxJyj2f5HB+3d/yJhzvvt4/xpS/a+vbF53B0y8qc13QObQfg/GV1fPrhDxN1I9Pz\nAOg0oaGllCprR7es4OgRLarRHNR0KNX+6vR173ZHe+mOdJF0819r7jfrrueJHaszF9XvXY3VZNLQ\nUkpNKyfMPjnvsbRCI6WXNx1Clb+aJ7pXa/dgBdDQUkrtEwoNjz9t3ms5jdemZ1BZ17fmgSmumhqH\nyrmSn1JKTSLHcbj8sC8DnFDquqjCNLSUUsrjOA56YnF509BSSilVMTS0lFJKVQwNLaWUUhVDQ0sp\npVTF0NBSSilVMTS0lFJKVQwNLaWUUhVDQ0sppVTF0NBSSilVMTS0lFJKVQwNLaWUUhVDQ0sppVTF\n0NBSSilVMTS0lFJKVQwNLaWUUhVDQ0sppVTF0NBSSilVMTS0lFJKVQwNLaWUUhVDQ0sppVTF0NBS\nSilVMTS0lFJKVQwNLaWUUhVDQ0sppVTF0NBSSilVMTS0lFJKVQwNLaWUUhVDQ0sppVTF0NBSSilV\nMTS0lFJKVQwNLaWUUhUjUOoKpIjIUcAlgANcZozZXuIqKaWUKjPl1NIKA58A/gwcV+K6KKWUKkNl\nE1rGmPuB5cB/AE+UuDpKKaXK0JR0D4rICuBqY8ypIuIDfggcCkSAC40xa0XkGOBR4PXAlcDHp6Ju\nSimlKsekt7RE5DLgWmz3H8BZQMgYczzwWeA73vI64KfAt4D/m+x6KaWUqjxT0dJaA7wV+KX3+ETg\nDgBjzEMicrR3/y7grimoj1JKqQo16aFljLlFRBZlLKoHdmU8ToiIzxiTHO+6W1vr97Z605Jul1y6\nTfLT7aIqTSmGvO/CBlfKHgUWQGfn7omp0TTS2lqv22UE3Sb56XbJT4O8vJVi9OAq4A0AIrISeKoE\ndVBKKVWBprKl5Xq3twKvEZFV3uMLprAOSimlKtiUhJYxZj1wvHffBT40Fe+rlFJqeimbk4uVUkqp\nsWhoKaWUqhgaWkoppSqGhpZSSqmKoaGllFKqYmhoKaVUmRORRSLydKnrUQ40tJRSahJ0tC1wOtoW\nOKWux3RTNlcuVkqp6cILq1Xe/RPaOja5Y7ykGAER+RVwJPAscB5wKXAmUA3cb4z5dwARuRt4EDgV\naAI+YIy5T0TuAT5mjHnSK3cf9rzZKuC/vNtB4AJjzIsicj7wZm/9S4FbjTGfmYDPssc0tJRSahw6\n2hZ8C3jHGMXmMHw5psGOtgVbxyh/c1vHpkvHKCPA+40xD4jIdcCHgWuMMV8GEJHrReRMY8xt2BmI\n/MaYFSKSukbha4DrgPOBT4rI/kDYGPO0iNQDJxljEiLyauDrwNu99z0MOByIAkZE/tsY0zFGXSeN\nhpZSSlWGTcaYB7z7vwI+Bqz3rllYDcwAngFu88rc4t0+Bizy7v8W+IKIXAq8H/iZt7wJuF5ElmED\nLzMb7jTG7AYQkee8dWloKaVUJfBaRKO2ijK7B4GJ6h7MXIfjPf4BcJQxpkNErsR276VEvNsE3r7e\nGDMgIn/HXoz3HdiuRoCvYMPpbBFZCNydZz2pdfkn4LPssTFDy2s2ngrsBySBl4B/GGOGJrluapxc\n18V1J+L/DaXU3mjr2OR2tC04IXV/glbbLiIrjTEPAucA92HndO0WkTpsCN1UxHp+gm2N3WOM6fWW\nNQBbvPtjTWJe0sElBUcPikitiHwD27R8H9CG7ac9D3hGRL7hbShVBlzX5dLfPs1FP3lIg0upMtDW\nscmdwMByAQN8xOuiawT+B7gW2yV4B/DQGK8HwBjzGNDLcNcgwDeBq0TkMWxLys143cjPUNIdjFNo\nBycit2A3yN+MMYkRz/mxI1beb4x5y6TXMj93Ol/AznVdBmNJ+iNxBqJx+iIJBqJx+iMJ+lO3kTj9\n0QR9QzEe39jDQMxeS7O5JsC5K9qZP6OGtqZqGquDJf40paUXO8xPt0t+ra3103qYuojMA+4yxkip\n67InRusefHuhKwp7IfYHEfnT5FSr8sUTSfqjCS90hgOmPzIieLzbkYE0EI2T3MPfMzsH4lxz17r0\n4/pwgLbmatqaqmhrrmZ+UzVtzdXMbawi6NdT9ZTaV4jIecBXgU+Wui57qmBLK8UbTbISuAH4EfbA\n3SeNMf+a/OoV5rqu29XVN1nrZiiWzGnh9OUJoIHoiOXec5F43rwfVXXQT23YT20oQG3YT00oQF3Y\nT204QE3IT513WxsOUOvd1oT91IXs8s/9/hkcn4/3rWxnS+8QHTsH2bxzkI6eQbbuipAYkYI+B2Y3\nVNHWVM38ZnubCrWmmiCOMz1+cGqLIj/dLvlN95ZWpSsmtP4FXIMdo/8J4ArgW8aYFZNfvcIuvPZB\n9+tvWZ53x5pIutktm2iCgYxAyfdcX7qMbe2Mt5Xj9znpIEkHihc0qQDKfG5k6FSH/Ph9e/f/iuu6\ntLbWky/M44kkW3dF6OgZtGHm3Xb0DNI7GM8pXxPyD4dYczXzm2yozWuqJhSorNaZ7pzz0+2Sn4ZW\neStmyHuVMeYmEfkJcIMx5l4RKflQ+Wc29/KenzzMfrPrGIwm6YvGGfACZyi2J60cHzWhAM21QeY3\nV2cEzcgWTiCnNVQb9hMO+EreMnEcp2AdAn6fDZ/malic/dzuoRgdO4eygmxzzyAvd/Xz0vbsAHSA\n1vow85urvVCrSnc3zqwNlXwbKKWmt2LCJy4ib8cOvLhCRM7CjtUvud2RBI9t7MXnkG69tDVVZ7V0\nasOB/N1qI7rb9raVU8nqq4IcMDfIAXPrs5Ynki7bd0fSLbPNO4dD7bGNPTy2sSerfHXQx7ymaq+7\ncTjU2pqqqQqW9NQOpdQ0UUxo/Tu2W/AjxpgtIvJO4MLJrdbYls2u43Ov25/acJCqYOlbOdOR3+cw\nt7GKuY1VHE1z1nP9kbjtauwZyupu3LRjkLWd/TnraqkLjQgz2+XYUh/Gp/92SqkijXlMC0BEGrDT\nfIB3JrYxZuNkVmwskzkQo5KV+jhF0nXpTLfOhtLH0Dp6Bunqi+aUDwV8zGvMHtXY1lTF/OZqakIT\n0wtd6m1SrnS75FeOx7REZBHwJ2PMIaWuS6kVMyPGt4GLgB0jnlqcp/iU0ZZVefI5DrMbqpjdUMWR\nC7OfG4wm2NI7SMdOG2abvRbalp5B1ncP5KxrRk3QC7HqrFCbVR/ep7tzVWVYeeVfHYAHv/RaPdt/\nAhXzU/YsoM0Yo80atVeqQ36WttaxtDV7IhXXdenuj2Z0Mw6H2jMdu3i6Y1dW+YDPYW6THQAyf0So\n1VUFctatM4SoqeYF1irv/gkTFFwjL01yIXYWjDd7lxG5ETvF3nUT8F5lq5jQehI7CaOGlpoUjuPQ\nUhempS7MYQuasp6LxBNsyehm3OwdQ+voscfPRmqsDqRDrK2xir8/t53qqgCXnbEfAZ8Pn+Pg9+Hd\nOhm39hjevtKC1zDfcyuv/Ou4L02y8sq/jnlpkge/9NrxXprkQuAS4Oci8t9A43QPLCgutH4JvCQi\nzwCpE3pcY8xpk1ctpaxwwM/illoWt9RmLXddl56BWM6oxo6eIV7YupvnXsk4VtMLF//y8aLez4GM\nMLPh5vM5+L1bnzMy8AoHYL7H6XU55Kw3+zVFrGuUMtnrHhHMwA/uWksg4OPTr96PcMBHMOCzt34f\ngX0ovCtMzqVJjDHf8QbHfR84tHRVmzrFhNb3gI8DmQMv9CeaKinHcWiuDdFcG+KQtsas52KJJFt6\nBvn6X15gS6+9qkJ9lZ8jFzTh4pBIuiRdN32bdBmxbORjl0QSr6xdFo+7JJMuCdfeZpZJJN2K+R/k\nwzc8kbPM50DQ7yPk9xEK2L+g37H3U8v8NugyH4cCTnp5KgCz15Fa7mQtT90P+n0lP1bpui4rr/yr\nM1p3ntciGrVVlNk9CExU92DOpUlExAEOBPqx19Paku+F00kxM2I8YYw5fIrqMx7TesLcPaUjwoal\nZr4PBv0Umj1lMt87f/iNHpJ5lyVdEi4Z4TiesLXLU/cTSZd/PL+dnQMxABqrAhzZ3kQs6RJLJInE\nk8QSLpF4gljcJZpIEo0niSaSxLz7ezonZjECPseGXWA4JIfDLl/QFQ7ArDAN+An5neF1jQxgv/1u\nXPrbpzHb+h548EuvPX5vP8tEDsTwRg+uA443xjzoTfbwHDa89sO2vP4fcJwxJneKm2mkmND6PjAX\nuB2IeYtdY8z1k1y3sWho5aGhlW20qa32VXsT5q4XfsNh5hKNDwdaKuDSQRdPEkkkcwIw6/nU6711\nRTPWFcsKzalrvz74pdeWVf+od2HGO4BHgaOwAzG+AvwaOMYY0y8i3wF2G2O+WLKKToFiugfrgF3A\nCSOWlzq0lBrTaFNb7ascx+Fbbz9kj8LccRwCfoeA30dNaJIqWEDSta3BWNzNCLr8AZhqMWaG4PDy\njKCM2zKReIK1nX1E4uXZsWuM2YDtBhxpeUaZT09djUqnYGiJSJUxZsgYc/5YZSalZkqpSVOJYe5z\nHMIBP+HTFiPyAAAgAElEQVSA/SU90VItULOt74GxS6tSGW267v8TkYtEpH7kEyJSLyIfwTZNlVKq\n4qVaoOT2KqkyMlr34DuBDwGPiEgvsBk75H0h0AL8F/D2Sa+hUkpNEcdxdAaLMlcwtLyrE39fRH4A\nHIYdoZIA1gJPGWP0H1YppdSUGnMghhdOT3h/SimlVMlU1iVolVJK7dM0tJRSqsyJyKoiynxCRKqn\noC4/F5G3TeV7ZioqtETkRBH5oIhUicirJrtSSilV6d5y65nOW249c0LOKzDGFDOi8eNAzXjWKyJ7\n0nBxGZ5SquB77uG6x1TMjBifwF6eZB52KOi/gOuMMd+ajAqNg86IkYfOiJFLt0l+ul3ym4iLQHph\nlZ578A9n37ZXA9dEpM8YUycipwBfBDqBg4HVxphzReRjwLcAA3QaY04XkTO8smHsALoLvJkz1mNP\nV3oNcBPwVmPMCu99FgF/NMYcKiJXAGcC1cD9xph/98r8DLgNmwnfHvGefcCPgFcDH8Fed/GjQAh7\nGZUPG2OShepWzLYoZkaM84EVwIPGmE4RORp4xNtASim1T3nLrWeO+9Ikb7n1zDEvTfKHs28bbRLe\nzNA7HDsTxivAKhE53hjz3yLySeAUY8wOEWkBPgecbowZFJHPAJ/CTv3kAl3GmKMARORdIrLIGLMe\n+DeGz7+9xhjzZa/M9SJypjHmtlR9jDHXiMinUu/pLa/BZsV/iMiBwGew8yUmROSHwHtE5PZR6jam\nYkIrYYyJiEjq8RDDlyhRSik1tR42xmwBO6E5sAi4f0SZldhgu9/bd4dGlPlNxv2bsGH1Dez5ue/0\nlp8mIpdig2gG8Ay2hTWaBPA77/7p2HkSH/XqUAVsxTaCRqvbqIoJrXu8iRjrROQs4GLgn8W+gVJK\nTSdei2jUS5NMdPfgCJGM+wkK78f/bow5p8BzmV1xvwFuFpFbsC2otSJSBfwAOMoY0yEiV2JDZyxD\nI87h/YUx5j8zC4jImWPUbVTFHCi7FHgJewXj84C/APvExIxKKbUnvJA6gYkPrNHsBhq8+w8BJ4jI\nUgARqRWR/fK9yBizDht+X2C4azAVUN0iUkfh7tDM9xzpTuDtItLq1WGGiLQDDxZbt3zGDC1vZowb\nsEH1SeBP2ANwqszoJdSVKh9/OPs2dwIDyy1wP9OPgTtE5E5jTCd2PMKNIvIktvtNCrwObGvrPdiu\nQowxPcC12C7BO7AhOOp7jqybMeZ54PPA37w6/A2YY4zpGmfdshQzevDbwEXAjszlxpjFxb7JJNHR\ngxlc1+Weq+8hGPBz/H+cWHEzeE8WvZ5WYTp6ML+JGD2oJk8xx7TOAtqMMWX1f/2+2KJIJpLEBmLE\nBmPE+mO4rsuMJTPSgbVz3U4A7vraXcxePhuf34fjc8ABx+cQqgux+FW5vzViAzE6HuvA8Tn2z7G3\nweogsw+enVM+Homzc/3OdLnUrT/kp6Ett6cgGU8y1DuUtX584PP7CFYHJ35DeVzX5Z6r7iEQ9HOC\nBrlS00IxofUktn+zrELrD5+5vWJbFMl4kr5tfUQHojaEvD/H77DklCU55Xdv3c3dX7ub+FD2oM3a\nWbWc8bUzcsq7CZcXb38xZ3nd7Lq8oTW0a4jHf/F43vKv+eprcpYP7hjkvm/fV3T5/s5+/nHFP4ou\nv3vrbu688s50yKVCt352Pad87pS861/1vVXZoejAwI4B4gN2m91z1T2cfPnJFfl9UUoNKya0fgm8\nJCLPMDzU3TXGnDZ51Rrb9he7uP3S21l88mIC4QD+oB9/yI8/6McX8tnHGcv8IT++oG/4fsA3YTuw\n+FCczY9szgkhX8DHURcclVN+qHeIO794Z87yqqaqvKEVrAlS21pLsCZIsCZIqCZEsDZIdbOdPcVx\nHE7+7Mnp7sFjP7qS3o29tjWa9I51JV38QX/e+lc1VHHkBUfiJl1wwU3a8oVaQaG6EAeceYAt563b\ndV1CdfkvZRuoCrBg5YLs8kmX6qZq3KRLIpYgEU0Qj8RJRBL0be+jbk4dbsKWSyaTuAmXeCTO8398\nPqtsIppgaPcQA90D6fP0XfufLDvX76TTdNIqrcSH4gz1DlE/J+dScUqpMlfMMa31wBXAxozFrjHm\nnsmr1th+fNb1e90/6Av6skItdev4HAa6B9I7/dRO0/E5LDltSXb5oJ9kIsnj1+e2VALVAc746hk2\nRAM2KMGG3DO/eyY7hGqChOvCtEjLHn+eZDJJa2s93d1FnVhelHyhkogmiEcz7kfiWctSjxMRb1nq\nfmp5NPv+RPGH/Fl//d39uLHsr0ljeyNNC5rYsGoDjfMbaTu6jbaj26ibPRnXwi1vekwrPz2mVd6K\nCa0njDGHT1F9ivaTd/zKPeJ9RxAfjBPtixIdiBIfitPU3kQimiAZS9qdYixBbDBGx+oOkvEkyXhy\n+Fe4A/Vz67PKJqIJW2YSOD4nuyU4IjALhWj6uXytyYznV/90NX6/j0POOdR+pgJBUQ6hEggH7P2w\nn0AogD+cvTy1LF0u32tSy8J2Gzi+7H2N67rcfdXdBAJ+Djx7OWvvXMuWx7aAC76Aj2Ri+LvQuKCR\n5WcvZ84hcybs85Y7Da38NLTKWzHdg/eJyO+A24GYt8w1xlw/edUaWzKWZPVPVmctc3wOR7z3iJxu\nPzfp8sqTrxCuD6dbNcGaIKHaEIe957Cc8slEkt2v7MYX9OHz+bJaG8lYMn0/6zaWIBkt8JwXhFmP\nY0li/TGGYkMkYgncxMQNLLn3qnv36HWZoRKuD096qEw2x3E45fJT0qMHW/ZrYaB7gLX/XMuGf20g\nGU/i+BzCjWF2dezC59eLHihV7ooJrTrsCWSpWYYd7O/TkoYWQLA2yIIVCwjVDgcRLraGGRyfwxu/\n+8ai1+vz+2ic3zixlR1DMpEsGHzpoMwTiMlokng0zsYHNhLdHQUg3BBm0asWEQgHckMlFT6p0AmV\nLlSmguM4WT9KambWcMg7DuHANx3Ixgc2suYfa+jfbrtTX/jzCySiCeYcOidrW2x6cBPNS5qpm7Xv\ndSGq6U1E7gY+bYxZPVbZclHMlYvPn4J6jNus/VsqdvRgPj6/z/7SL2ailDwOfvvBep7WOASqAiw5\ndQmLT17Mtme2seYfa+h8vpPuF7upba1l6elLaT+hnWQ0yaM/fRRcaFrYRNtR9hhYbWttqT+CKnM/\nPut6B+Di359XzufnZF5mpCIUPKYlIn82xrxRRF7O87RrjMkd5jaFXNd19YTRbHoibX7FHrvp3dzL\n2n+uZdMDm0jGkwSqAyxYsYCamTV0mS62P7893Y07+5DZHP+x4ye76pNKj2nlNxHHtLzASs89uLfB\n5V0y5A7gUeBI4FnstHqXkv/yIXdjp0s6FWgCPmCMuc+7YOPPgEOBF7CzG33YGPOYNwv7Md66fmuM\n+aK3rquBN2FHj//NGDPqvIuTbbSW1kXe7SnkdLiVPpm1JZFrZFeYGp/G+Y0ced6RHHT2Qay/dz3r\n7lrHy3e/DA7MPXwuKz60gsiuCB2rO7SlNU25rsuPz7reGS1kfnzW9eO+NMmPz7p+zEuTXPz788YK\ng/2x1516QESuAz5M4cuHuIDfGLNCRF4PXIm9ftaHgD5jzHIROQR4LGP9nzPG7BQRP/AP7/ktwFnG\nmAO89yg0z+CUKRhaqanvge8aY96W+Zw3z9Tpk1kxpUolXB9G3ijs99r96Hi0gzX/WMMrj7/CK4+/\nQmN7I8tOX8a8o/NPv9mxuoOBrgHmHTWP2hYNtkqSmlkG20Iqx2b0JmPMA979XwEfA9aLyGXY1tHI\ny4fc4t0+hr18CcBJwH8BGGOeFpGnMtb/byJyETYX5gIHAs8BQ15I3sbYlyaZdAVDS0RuxV5sbN6I\nLsIA2edsKTUt+QI+FqxcwPwV89mxdgdr/rGGLY9tYfXPVvPM755hySn2mFi4IZx+zbp/rqPrxS6e\n+e0zNC9uTh8Dq5k5rqugqykw2DNIz4Yeejf2EqgOsPmRzamp0I4b7XVei2jUVtFEdw96MteRGhA3\n2uVDUpcwGXn5kpzuGBFZjJ0U/WhjTK93deJq7+KNx2IbKW8HLqHEDZbRugfPB5qB/8ZeLjn1QePY\nC3kptU9wHIeZy2Yyc9nMrCHzz//xecxfDPNXzGfZ6ctoXNDIsR88li2Pb2HL6i10vtDJzpd38sxv\nn+HUz59K08KmUn+Ufd7urbt5+qan6dnYQ6R3+LJU9fPqCVQVM5i6OBf//jz3x2ddf0Lq/gSttl1E\nVhpjHgTOAe7DtggzLx9y0xjruNd77V0icjD22BbYy4v0A7tEZDbweq9MLVBrjLldRO4H1k7QZ9lj\no3UP9gK9wJunrjpKlbd8Q+Y3rtrIxlUbaZEWlr16GYtOXMTiVy0msjvClse30Pl8J40LpvYUin2V\n67r0d/Yz0DXArOWzcp4PVAXY9vQ2qmdUM/fwuTQtbLJ/7U2EG8KpiacfyLPqcZuEUYMG+IiI/BQ7\nEON/sA2LZ7ANiUKXD4HhVtr/AD8TkeeA57EDOzDGPCkij2MHZ2zCBiJAPfAH76KQDvbyVCU15owY\nZUwvTZKHjgjLNZnbxE26WUPmgawh88GqwrPYD+wY4JEfP0LbUW3MO2oeNTOmtgtxOnxXkokkHY92\n0LOxJ93VFxu0836+6Zo3padOyxTZHSFcH86zNht6v7/4975yG6bujR78kzHmkFLXpdQmrj2s1D7I\n8TnMOXQOcw6dkzVk/qlfP8Vzf3iOhScsZOlpS/OONtyxdgc71u1gx9odPH3T08xYOsPOhXhkG9Uz\nqkvwacpXMpEcnsE/g+NzePKGJ4kN2Ml66mbXMfvg2TQtbCKZSOYNrUKBBbYruNwCK0O51mtKaUtr\nmpkOv54n2lRvk8juSHrI/FDvUHrI/LJXL2PmfjOzdrxDu4Z45bFX2PzoZrpe7AIXlr1mGYe8c/J/\nUJfrdyUZt9OopVpPPRt76N3Uy6lfODXvzPwdj3YQbgjTuKBxQq7PpnMPljcNrWmmXHdEpVSqbZKM\nJ9ND5ns29ACkh8y3HdOWc6mYoV1DbHlsCy37teS9mGYilih4eZk9Ua7flXu/dS/dL3anHzs+h/p5\n9Rzx3iOYsWTGpL+/hlZ509CaZsp1R1RKpd4mrutmDZnHtfND5hsyP5q7v343Pr+PtqPtMbDqpr3r\nQpzq7ZKIJujd3JtuPS1YuYBWac0p99JfX6JvW196gETD/IYJDeuxaGiVt7I5piUipwP/BtQA3zTG\nPDXGS5SqCOMZMl9IqpXV9VIX3Wu6eeo3TzFz2Uzajmpj8cmL8x67KRcbVm1gzd/XsPuV3fZCo57q\npuq8obXfa/ebyuqpClM2oYU9ke1iETkcOAPQ0FLTTjFD5kfOMg/gD/o56dKTGOoZYsvjW9j86Ga6\nX+pmcOcgS04r6TSgxAZi9GzqwR/05+2+S0QT9Hf107ykmeaFzfZCnO1N1M/VK0er8Sur7kHvRLZr\ngMuMMV1jFNfuwTxK3RVWjsp5m+zNkPnBnkEGugaYuWxmznORvghuwqWqsfBlA/Z0u/R39dth5l43\nX+rSLvOOnMeKD63IKZ+IJfD5fRVz6RvtHixvU9LSEpEVwNXGmFNFxAf8EHsmdgS40BizVkRagG8C\nVxQRWEpNC3szZL66qbrgca3196znuT88R8v+LfYY2JHzqGoYDjDXdRnrB2s8EicQzt1FDHQP8Ozv\nngUgWBOk9cBWmtqbaJGWvOuZyuNRavqb9JaWN5njudiZhY8XkbcCZxpj3u+F2eXGmLNE5BdAC7AD\n+L0x5ndjrFpbWnmUc6uiVCptm4xnyHwhmx/ZzNp/rmXHmh12gQMt+7dw0FsPonlxc86111Lz8KVa\nTz0beghWB3n1l1+ds+74UJxtz22jqb2Jmpk10+7KAtrSKm9T0dJaA7wV+KX3+ETsdWEwxjwkIkd7\n9983BXVRquwVM8t8viHzmeYfM5/5x8xncMcgHY910PFoB12mC1/Al5qqCIB7rr6HlR9eyR2X3pH1\n+qrGKupm1+Em3ZxuvUBVgLYj2yb+gytVhEkPLWPMLd4UJCn1wK6MxwkR8RljkuNdd2urHsjNR7dL\nrkrdJrPf1MgRZx7Ithc6efpPz7P+wY2s/tlqnrv1WZa/Xlj+2v1HH/reWk+7zIJ3H0F/9wDVzVU8\n+5un008HA37mL21h/9OX0jC7npYlM2hZMmPKp5RSqlhTMhDDC60bjTHHich3gAeNMTd7z20yxizY\ng9Vq92AeldYVNhWm0zbp7+pn3V3r2PCvDek59ooZMp8pdd2ozO5BNUy7B8tbKYa8r8JeuvlmEVmJ\nDm1Xqmi1LbUc8o5DOOBNB7Dx/o2svXNtUUPmMzmOw8mfPZnW1nq6uvqmsPZK7b2pDK1Uk+5W4DUi\nkrpA2gVTWAelpoVgVZClpy1lySlL2Pr0VtbeuZbO5zvpMl1FDZl3nNzJZ5WqBGV1ntY4afdgHtOp\nK2yi7CvbJHPIfDKeJFAdYNGJi1hy6pK8Q+b3le0yHuV6aRI1rJxmxFBK7YXG+Y0ced6RHHT2Qekh\n82v+voY1/1iTM2S+mPO09jWpY33YQxjHl7g6qgANLaWmmbGGzC89bSnr7l5HKBQo2UAM13Vxkxl/\nCZdkMpm+7yZdkolk/jKJwq8Z+br0/dQ6R5b31pFMJNn00Caiu6IAx035BlFF0+7BaUa7fHLt69sk\na5b51Vuyngs3hGk7pg2SXpAUGwz5yiSygygrdPKUKWcX//48PeBXprSlpdQ0lznLfF9nH/defS+R\nXREAIrsirLtz3Z6t1+/g+Bx8Pl/6furP5/fhD/oLl/F7y7z7qddkPp/vdekyma8fZR2F6pbvPXBg\n9c9Ws7tj9wMTuf3VxNKW1jSzr7cq8tFtks11Xe7++t2QcDn03MPw+/2jBgM+snb+Pr8PHKbl6EMd\niFH+NLSmGd1B59Jtkst1XT1PqwA9ubi8le+V45RSk0bP01KVSkNLKaVUxdDQUkopVTE0tJRSyuO6\nLh1tC7TftIxpaCmlFDawYuedA3ZGDFWm9DwtNa3pdEWqEDceh62v4G7cSHLjBpLX/gi6u0BnxChr\nGlpq2kr9cu4M+uG6X+pouX2QG4tCRwfuxo24mzbibtrg3W6CLR0Qj5e6imqcNLTUtJQcHCR+3rvh\npReJAbz5dfi/8GV8s2fDrNk41aNc7VdVFHdoCHfzJtiUCqaN6ZBi6yuQzHNR9OYZOMsPxmlvx1nQ\nDgvaYf4CEld/FZ57VmfEKGN6cvE0sy+dSOsmErZ7Z8P64b/19pZXtoz+4oYGnNlzYNYsnFne7ew5\nOLNmQeq2oXFat84q6bvi9vfbEEoH06Z0QLFta/4XtbbiLGhP/7GgHad9Ic78BTj19fnfx3WJHXGQ\nr61jU8XuGKc7bWmpsua6LuzcibvhZS+YNthQ2rDe7rCi0dwXtbbiHHUMLFyIe/8qfNVhePd7obMT\ntm3F3bYNt3Mb7pYOeOlFCu6dqqpsq2zWbJxUC222fZxazsyZOH7/JG6BfYe7a9dwEGW0ltxNG6C7\nO/cFjgNz5uAcuyI3nBYswKmumfoPoSadhpYqC+7gQHYgpf82wO5duS+orcVZtj/OwoU4CxfBosU4\nCxfZX9K1wxc8HGu6IrevD7Zvw92+DXfbtuH727fbgNu+HXfjhsLB5vdDS+twqM2andFa88KudRZO\nOLy3m6jipX+AbNqIu9mGUmaXHj09uS/y+WBeG85xBwwHU6pLr23+hG7XEaMH9XpaZUpDS00ZNxaD\nLR3pcHI3ZnTnbd+W+4JAANrbcY462gZS6m/RIpgxs6iuu7GmK3Lq6qCuDmfJ0sL1jkahsxN3+9as\nMEuH2vatuM89C089WTjcmptxWoe7HvN1TVJXV/Hdka7rQleXHfCwceOILr2N0Jfnx0MgYAPo4EOz\nQslZ0A7z5uEEQ5NX3x07SN7+Z5IvvoD719thaAh09GBZ09BSEyq909qwHnf9yzaYUi2ozZvyj9aa\nMwdnxUqchV5rKdV6mjsPJ1D6r6gTCkFbG05bW8EybjIJO7qzw2zbVkiF2vbtdqf9oikcbNXVw6FW\n6DjbjJk4vtKeXukmk7ZFmtWFN9ylx9Bg7ovCYZz5C+Do7FByFrTbzzaJ/85uPA7bt+HMy/Pv199H\n4ltXTdp7q4lX+j2CqkhuX1/WAIjMLj0GBnJf0NCAs/wgnHYvlBYthoWL7I5rGozkc3w+203Y0grL\nD8pbxnVd6OvLCjO8cMvqmlz/cuFgCwTsMbtZc7KOsw13Tc6GWa1jtk7GOn8t8xwmN3NU3qYNsHlz\n/mOJ1dU47e3QvjArlJz2dttFOgVh6yYSuI89ivvii7gvvoD70ou4a9dAMEjwXw/ltmTb5uO/6ls4\n++0P7QuJf+B98PSTOnqwjOnowWlmIkeEubEobNqUdXzJdum9nP/AeDiM077QhlH7Qq8rz2s9NTVN\nSJ32RCWNkgM7hJvO7RmttW3e4JGMrsmuTkgkCq9kxswCx9lmwazZxL9wOYFQAK78Wv6h4oXOYaqr\nt/+27XaIeNYxppktU9a96cbj4PfnvJ+bSBA74ZhUNx+EQjhLl+Hstz/+yz8/5uAMHT1Y/jS0ppnx\n7qDTXT0Zw8VTx5vo6Mg9x8Vx7IHxzGNMXpcec+aWvOsqn0oLrWK4iQR0d9tQ6/RaayO7Jju3D++8\nx6O5Oc9Q8XacBQuhcepPA3B7enBfMrjG2NsXDe7aNQT/eDvOnLk55RM3/gqamvHtL/YH1Di7HvV6\nWuVNuwenkdG6fNzenuEBEN7xJjZuwN24If+ObcZMnMMO9wJp8fBxpvkLdCRcGXD8fm8gx6yCZVzX\nhV29WWGW3PoK7s2/hp07baGmZnznnGv/nRe023OYGhqm6FMUJ/6hC3Gff254gdd6cnt784aW/93n\nTmHt1FTTltY04LoubixG/IJz8ScT8P6LbSClz2t6Of9w4urqdEuJ9ACIxbb7p8x2XHtjOra09kZq\naHewhNNbuT099phTxrEn/8c/hW9l7kjzxK+ux93RhbP/Afj223+PWk/joS2t8laxoeW6rjsVlwp3\nk0nbtx+L2YPPsZj9i8fsEO7MZem/qH0u8y8ahbi972Yu89aVKueOeI/Mcm7m8pHvW4jfb4cTLxpu\nLdnzmRbZX+oVPsS6GBpaucY6f20yxa/6Ksnf3JC9MBzG/59X4H/L2VNen5E0tMpbxXYPbj3hRDj3\nApxEPP+O3QuPzB27m2eZXZ4neKJeOJR6Qs1gMPevuganoTH92A0E4EUD/f32NXPm4r/88/gWLbbH\nn4LB0n4GVXbGOn9tT7g7d2Yde3JWHIf/jW/KfW85AOekk3H22x9HvNZT+8KyOL1Blb+KbWl1tC3Y\nu4o7DoRCw0EQ8G5DIbuTH7EsVc5JLc98beZzwSAEM54L2fJOZtkR7+vkW556rsgdi+u6xN77bgJB\nP85Pf7VPtKCKpS2t/CZquyT+ejuJb19tp8nK4HvzWQS+/PW9Xv9U05ZWeavsnzYL2vF/9BP25M88\ngZETBoGMgJmOv+ocx/4pNYZirzPm7tiRHrFHXT3+s9+WU8apqbHDz086GWd/wdlf7Mi9Be2TUXW1\nj6vcltZ+4gbuexjfJAyxTs2Rlj6WlTp+lUjgkwNyyycSJP/xt6xjU8TjkHTxv+e9ueVjMRJXfXW4\nfDxu1w8Ev/f93PLRKLG3vWm4Pqn1Ow6h+x4enjPt6SftCw45FJ59BkJh29ILhWyYV1cRuuW2vPVP\nfO6z2WVDIQiHCXz4o7nlk0ncu/85/IMg9Zpw2Hb1lJFSHrspV2MNxHDXrSX+7W/gvmSyWk/O8oMI\n3nBz3vVNp5a9trTKW+U2NwYGiL/6ZJxXnYKTSHiDEVwCV387p6gbixF/99vtCYmpHb4XEqF//it3\n3fE4sdNOzF3u9xNa/XTu8mSSxGc+nbd8vtDC5yN5S+7//BQKYL8fIlE7G0JtnW0lpgIjH9eFgw/F\n8QZsuNEoRCMwWOCcnWiU5B1/zl0eDkOe0CIaJf6pj+UuDwYJPfJkbnWiUWLHH50Tck5tLcGbf59b\nPhYj8YX/zAlRp7oa/wc/kls+kcC9567sAA2FcEMhEl++MucikG4yaaeUSv1gc1375/PZkZT51v/S\ni4ALbnZ534HL85d/6kmvfMZr/D58RxyVWz4ex334weH1pl7j9+E74aS828e956502dQPT8fvx/fq\nM/KWT97+5/RnT177I+jYbK8zdt45BK+/ITt0qqpw77/PTq/1qlPssaf9Bd8BB+asG5hWgaXKX+W2\ntAoc0wo+/mzuL0fXJXbyccPdg95O3wkGCdx0a97y8Us/acMhEISgdxsI4L/s8rzlkzf/Jr1eAgHv\n2FcA5+RT8/5P7a5/Oat8+nU1tTlli7E3w5jdZNL+oo5Fh0MuFoV4At8RR+aWj0VJ/ubX2eWjEQAC\nn/5MbvnBAeL/fmHu+gNBQrf9Nbd8f7+d1WCk6mpCD6zOLT/QT+z4POVxIDUh0iGHpXfOBddfU0Po\n/keLr0+llwc45FCC19+Y9X1Jnd/lNJZuFpNS0pZWeavYlpb/gAPgK1fjC4XsTj8VLnk4jkPo3geL\nXrfjOAS//b1xlfe/811FlwdwFi0eV/li6hC8/oY96gpzfD6YPXv48VjlgyH8555X/Pqrawhef8PY\nBVOqqwn+9a4RoRgtPG2RP4D/05/JKu9GI7h3/AW6unLLBwL4znqrd/zPOw7oYLtT8wkE8L373OFj\nhqnyheb3CwbxfeBi78MPv6bgKM5QEP8lH89eP479IVOo/GWXZ5d1Risfwv/Fr3gPHFwg+ZP/xR8M\n4Fx3fc4PHMdxYB8NLFX+KralNVXnaVUaHSk3rBxOoi1XeqyvMG1plbeKbWnpDkiNZW9an9PdZJyn\npdRUKL/ZTZWaQLpzVmp60dBSSilVMTS0lFJKVQwNLaWUUhVDQ0sppVTF0NBSSilVMTS0lFJKVQwN\nLWfoZFkAAAeYSURBVKWUUhVDQ0sppVTF0NBSSilVMTS0lFJKVQwNLaWUUhVDQ0sppVTF0NBSSilV\nMTS0lFJKVQwNLaWUUhVDQ0sppVTF0NBSSilVMTS0lFJKVQwNLaWUUhVDQ0sppVTF0NBSSilVMTS0\nlFJKVQwNLaWUUhVDQ0sppVTF0NBSSilVMTS0lFJKVQwNLaWUUhVDQ0sppVTFKLvQEpHTROTaUtdD\nKaVU+Smr0BKRpcDhQFWp66KUUqr8lFVoGWPWGmO+W+p6KKWUKk+BqXojEVkBXG2MOVVEfMAPgUOB\nCHChMWbtVNVFKaVUZZqSlpaIXAZcC4S9RWcBIWPM8cBnge9MRT2UUkpVtqnqHlwDvBVwvMcnAncA\nGGMeAo7OLGyMee8U1UsppVQFmZLQMsbcAsQzFtUDuzIeJ7wuQ6WUUqqgKTumNcIubHCl+IwxyXGu\nw2ltrR+71D5It0su3Sb56XZRlaZUrZtVwBsARGQl8FSJ6qGUUqqCTHVLy/VubwVeIyKrvMcXTHE9\nlFJKVSDHdd2xSymllFJlQAc/KKWUqhgaWkoppSqGhpZSSqmKUaoh7xNORA4DrgHWAr8wxtxd2hqV\nDxGZDdxmjDmm1HUpFyJyFHAJ9oT3y4wx20tcpbIgIqcD/wbUAN80xujIXo+InAa82xhzUanrsi+b\nTi2tY4FXsCcxP1viupSbS4H1pa5EmQkDnwD+DBxX4rqUk2pjzMXAt4EzSl2ZcqFXoCgf0ym07gMu\nBL4J/EeJ61I2RORDwK+AoVLXpZwYY+4HlmO/K0+UuDplwxhzm4jUAh8Dfl7i6pQNvQJF+Sjr7sFi\nZoYXkS8D+wF/xLa0eijzz7W3xrldZnnPHSsibzPG/K5kFZ9k49wu3wUeBV4PXAl8vETVnnRFbpev\nAMuw2+Fq4ApjTFfJKj0FxrldPmSM6SlhdZWnbHfu3szw5wJ93qL0zPDel+07wFnGmCu88sdhj2nF\ngC+VoMpTYrzbJeN110/zwBrv9+VU4KdAFPjfElR5Soxju3zBK/8LoAW4SkR+P12/M+PdLqp8lG1o\nMTwz/C+9x1kzw4vIyJnhHwAemNIalsa4tkuKMea8qaleyYz3+3IXcNeU1rA0xrtd3je11SuZPf3/\nSK9AUWJle0xLZ4bPT7dLfrpd8tPtkp9ul8pVSf8oEzEz/HSk2yU/3S756XbJT7dLhaik0NKZ4fPT\n7ZKfbpf8dLvkp9ulQpTzMa0UnRk+P90u+el2yU+3S366XSqMzvKulFKqYlRS96BSSql9nIaWUkqp\niqGhpZRSqmJoaCmllKoYGlpKKaUqhoaWUkqpiqGhpZRSqmJoaCmllKoYGlpKKaUqhoaWmnAiMuZE\noyIyqZcFEZEvisiVY5R5u4j8bALf8ygRuda7f7GIvGui1q2Usiph7kE1PZ08yeuf8vnJjDGrgYu8\nh8ezb1yvS6kppaGlJo2InAL8J9APHAg8DZyDvSosIvKAMeY4EXkd9mrTQeBl4CJjzA4RWQ88CByO\nnYX7OWNM6rW/Bf4PeAl7xepaYBbwHWPMNaPU6T3A57FXrF0DDHnLjwG+C9QAXcC/G2PWi8jdwEPA\nSUAr8FFjzB0icg5wKZDw6nwucBxwJfBV4M3AKSKyE7gOWGKM2S0ii4DbjDEH79FGVWofp92DarId\nB3wEG1rtwBnGmI8BeIHVClzlLT8S+Bv/v727CbEpDuM4/mWa8pKNJbMZxjyznUajkSSRpcbLMBuL\nsWFHNkiI1BAWtiSjsfCSjIksRilDaRZeIn4bNpKJWJKMa/H/D8d1R0Zuuvl9Nve83HP+z7ndenqe\n/6k/HM7XloDrklpIiWkjQETMyve9BmwGDkhqB5YDh/K1U8oDiYg5wFFgGbAImA6UIqIeOAV0S2oj\nJa+ThRjqJS0GtpMSEsBBYKWkhcAzoGV8HEk3gQFgr6SrOc51+fQmoG8Sv5+ZFThpWbU9lvRKUgl4\nCswuO7+IlMxuRcR9UoJrKpy/ByDpATAtIuYDncCgpE/ADmBGROwkJayZv4hlMXBH0mhe4O8MKbk1\nA/OAwRxDL9BYuO5G/nxSiH8QuBsRR0iV08MK440nztPA+DLt3Xxf4t3MJslJy6rtY2G7xM8VUB0w\nLKlVUivQDnQVzn8obPeTqq2uvA1wEVhNSii7CvevNKf1hR//82OFGJ4XYmgDllZ4hm/xS9oGrAXe\nAf257Vg+5vj+bWBuRHQCLyS9rhCbmf0GJy37V8Yioo5USXVExIJ8fA/f24PlzgEbgCZJw/nYCmCf\npEFS24+ImEqF9iBpXqwjIhoiYgqp6imR2nuzI2JJ/l5PHquiiJgaEQLeSuoFzpLm3Yo+k+boyFVm\nH3AC+GtvK5r9j5y0rBpKE2wXDQAPgPekJHEhIh4BraSW308kvQTeAJcKh/cDw3nF2RZSC7Ixj1sq\nu34U2EqaNxshV1C5zbgeOBYRD0nzTj0TPVtuLe4DhiJihPSSxvGy5x0CdkfEmrx/nvSSx5UJ7mtm\nv8ErF5tVWa78tgDNua1oZn/Ir7ybVd9loAFY9a8DMat1rrTMzKxmeE7LzMxqhpOWmZnVDCctMzOr\nGU5aZmZWM5y0zMysZnwFmiwcrlraKo0AAAAASUVORK5CYII=\n",
"text/plain": [
"<matplotlib.figure.Figure at 0x227f1ae10>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"plt.figure(figsize=(5, 4))\n",
"ax = plt.gca()\n",
"palette = sns.color_palette('Set1', 4)\n",
"df.loc[('construct', 'int'), :].plot(logy=True, logx=True, marker='.', ax=ax,\n",
" color=palette, legend=False)\n",
"handles, labels = ax.get_legend_handles_labels()\n",
"df.loc[('construct', 'float'), :].plot(logy=True, logx=True, linestyle='--', marker='.',\n",
" ax=ax, color=palette, legend=False)\n",
"ax.legend(handles, labels, bbox_to_anchor=(1.0, 0.5), loc='center left')\n",
"ax.set_ylabel('time (ms)')\n",
"ax.set_title('Construction time (lower is better)')\n",
"plt.savefig('intervaltree-construction-benchmark.png')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Solid lines show performance on integer data; dashed lines show performance on floating point data."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"##### Querying"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Depending on the interval density, bx-python or pandas offer the best performance for querying. However, the pandas IntervalIndex offers the best performance under the worst case scenario of high interval density."
]
},
{
"cell_type": "code",
"execution_count": 321,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<matplotlib.figure.Figure at 0x227de8e10>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"plt.figure(figsize=(5, 4))\n",
"ax = plt.gca()\n",
"palette = sns.color_palette('Set1', 4)\n",
"df.loc[('query', 'int'), :].plot(logy=True, logx=True, marker='.', ax=ax,\n",
" color=palette, legend=False)\n",
"handles, labels = ax.get_legend_handles_labels()\n",
"df.loc[('query', 'float'), :].plot(logy=True, logx=True, linestyle='--', marker='.',\n",
" ax=ax, color=palette, legend=False)\n",
"ax.legend(handles, labels, bbox_to_anchor=(1.0, 0.5), loc='center left')\n",
"ax.set_ylabel('time (ms)')\n",
"ax.set_title('Query time (lower is better)')\n",
"plt.savefig('intervaltree-query-benchmark.png')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 2",
"language": "python",
"name": "python2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.10"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
@alexlenail
Copy link

alexlenail commented Jan 31, 2017

This is wonderful. Does there exist a tutorial for interval trees with pandas somewhere? I'm new to pandas, and having a hard time finding documentation about pd.interval @shoyer

@shoyer
Copy link
Author

shoyer commented Jan 31, 2017

This gist documents a feature that was never merged. See this PR for the unfinished code:
pandas-dev/pandas#8707

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment