├── .gitignore ├── 0-Intro.ipynb ├── 1-Memory-Profilers.ipynb ├── 2-A-Reduction-Tale.ipynb ├── 3-In-Memory-Tables.ipynb ├── 4-On-Disk-Tables.ipynb ├── LICENSE ├── README.rst └── movielens-1m ├── README ├── movies.dat ├── ratings.dat.gz └── users.dat /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /0-Intro.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Using Data Containers With Python" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Data containers (or data structures) are an extremely important subject in data science in terms of both:\n", 15 | "\n", 16 | "1. Performance\n", 17 | "2. Storage capacity required\n", 18 | "\n", 19 | "During this tutorial you will learn how to do measurements on the parameters above on a variety of data containers in Python. Another goal is to get insights on which situation you should be using one data container or another." 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "## Caveats for following the tutorial:\n", 27 | "\n", 28 | "1. These notebooks have been created and tested mainly on Jupyter notebook 4.1 and Python 2.7, but Python 3.4 or 3.5 should work equally fine, except for some profile utilities which will be seldom used.\n", 29 | "\n", 30 | "2. You can follow the tutorial by re-playing the [provided notebooks](https://github.com/FrancescAlted/DataContainer/releases). For those of you with problems with the Wifi, there are pendrives available.\n", 31 | "\n", 32 | "3. **In case** you cannot reproduce the desired results in your own laptop, do not worry too much; my advice is that you just concentrate in tutor's explanations and ask in case something is not clear enough." 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "## Requisites\n", 40 | "\n", 41 | "* IPython notebook (aka Jupyter)\n", 42 | "* NumPy\n", 43 | "* pandas\n", 44 | "* numexpr\n", 45 | "* tables (aka PyTables in Anaconda)\n", 46 | "* bcolz\n", 47 | "* psutil\n", 48 | "* memory_profiler\n", 49 | "* ipython_memwatcher\n", 50 | "\n", 51 | "These are all in Anaconda (bar ipython_memwatcher) or in PyPI repo.\n", 52 | "\n", 53 | "### Optional\n", 54 | "\n", 55 | "* guppy\n", 56 | "\n", 57 | "These are not in Anaconda, but in PyPI. In case you are using Python 3 (3.4 recommended) you may have some problems because some of the optional packages do not support Python 3 yet." 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "## Contents" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "1. [Memory Profilers](1-Memory-Profilers.ipynb)\n", 72 | "2. [A Tale of a Reduction](2-A-Reduction-Tale.ipynb)\n", 73 | "3. [In-Memory Tables](3-In-Memory-Tables.ipynb)\n", 74 | "4. [On-Disk Tables](4-On-Disk-Tables.ipynb)" 75 | ] 76 | } 77 | ], 78 | "metadata": { 79 | "kernelspec": { 80 | "display_name": "Python 2", 81 | "language": "python", 82 | "name": "python2" 83 | }, 84 | "language_info": { 85 | "codemirror_mode": { 86 | "name": "ipython", 87 | "version": 2 88 | }, 89 | "file_extension": ".py", 90 | "mimetype": "text/x-python", 91 | "name": "python", 92 | "nbconvert_exporter": "python", 93 | "pygments_lexer": "ipython2", 94 | "version": "2.7.11" 95 | } 96 | }, 97 | "nbformat": 4, 98 | "nbformat_minor": 0 99 | } 100 | -------------------------------------------------------------------------------- /1-Memory-Profilers.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Introduction to Memory Profiling" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "> Objectives:\n", 15 | "> * Be introduced to memory profiling using different tools\n", 16 | "> * Some small introduction to time profiling in IPython too\n" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## ipython_memwatcher" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "Our recommended way to profile memory consumption for this tutorial will be [ipython_memwatcher](https://pypi.python.org/pypi/ipython_memwatcher):\n" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 1, 36 | "metadata": { 37 | "collapsed": false 38 | }, 39 | "outputs": [ 40 | { 41 | "name": "stdout", 42 | "output_type": "stream", 43 | "text": [ 44 | "In [1] used 0.027 MiB RAM in 0.003s, peaked 0.000 MiB above current, total RAM usage 34.930 MiB\n" 45 | ] 46 | } 47 | ], 48 | "source": [ 49 | "from ipython_memwatcher import MemWatcher\n", 50 | "mw = MemWatcher()\n", 51 | "mw.start_watching_memory()" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 2, 57 | "metadata": { 58 | "collapsed": false 59 | }, 60 | "outputs": [ 61 | { 62 | "name": "stdout", 63 | "output_type": "stream", 64 | "text": [ 65 | "In [2] used 31.121 MiB RAM in 0.275s, peaked 4.094 MiB above current, total RAM usage 66.051 MiB\n" 66 | ] 67 | } 68 | ], 69 | "source": [ 70 | "# Let's create a big object\n", 71 | "a = [i for i in range(1000*1000)]" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 3, 77 | "metadata": { 78 | "collapsed": false 79 | }, 80 | "outputs": [ 81 | { 82 | "data": { 83 | "text/plain": [ 84 | "Measurements(memory_delta=31.12109375, time_delta=0.2749760150909424, memory_peak=4.09375, memory_usage=66.05078125)" 85 | ] 86 | }, 87 | "execution_count": 3, 88 | "metadata": {}, 89 | "output_type": "execute_result" 90 | }, 91 | { 92 | "name": "stdout", 93 | "output_type": "stream", 94 | "text": [ 95 | "In [3] used 0.035 MiB RAM in 0.048s, peaked 0.000 MiB above current, total RAM usage 66.086 MiB\n" 96 | ] 97 | } 98 | ], 99 | "source": [ 100 | "# Get some measurements from the last executed cell:\n", 101 | "meas = mw.measurements\n", 102 | "meas" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 4, 108 | "metadata": { 109 | "collapsed": false 110 | }, 111 | "outputs": [ 112 | { 113 | "data": { 114 | "text/plain": [ 115 | "31.12109375" 116 | ] 117 | }, 118 | "execution_count": 4, 119 | "metadata": {}, 120 | "output_type": "execute_result" 121 | }, 122 | { 123 | "name": "stdout", 124 | "output_type": "stream", 125 | "text": [ 126 | "In [4] used 0.004 MiB RAM in 0.008s, peaked 0.000 MiB above current, total RAM usage 66.090 MiB\n" 127 | ] 128 | } 129 | ], 130 | "source": [ 131 | "# MemWatcher.measurements is a named tuple. We can easily get info out of it:\n", 132 | "meas.memory_delta" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 5, 138 | "metadata": { 139 | "collapsed": false 140 | }, 141 | "outputs": [ 142 | { 143 | "data": { 144 | "text/plain": [ 145 | "32.632832" 146 | ] 147 | }, 148 | "execution_count": 5, 149 | "metadata": {}, 150 | "output_type": "execute_result" 151 | }, 152 | { 153 | "name": "stdout", 154 | "output_type": "stream", 155 | "text": [ 156 | "In [5] used 0.016 MiB RAM in 0.010s, peaked 0.000 MiB above current, total RAM usage 66.105 MiB\n" 157 | ] 158 | } 159 | ], 160 | "source": [ 161 | "# This takes between 32 ~ 35 bytes per element:\n", 162 | "meas.memory_delta * (2**20) / 1e6" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": 6, 168 | "metadata": { 169 | "collapsed": false 170 | }, 171 | "outputs": [ 172 | { 173 | "data": { 174 | "text/plain": [ 175 | "int" 176 | ] 177 | }, 178 | "execution_count": 6, 179 | "metadata": {}, 180 | "output_type": "execute_result" 181 | }, 182 | { 183 | "name": "stdout", 184 | "output_type": "stream", 185 | "text": [ 186 | "In [6] used 0.000 MiB RAM in 0.008s, peaked 0.000 MiB above current, total RAM usage 66.105 MiB\n" 187 | ] 188 | } 189 | ], 190 | "source": [ 191 | "# What are these elements made from?\n", 192 | "type(a[0])" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 7, 198 | "metadata": { 199 | "collapsed": false 200 | }, 201 | "outputs": [ 202 | { 203 | "data": { 204 | "text/plain": [ 205 | "24" 206 | ] 207 | }, 208 | "execution_count": 7, 209 | "metadata": {}, 210 | "output_type": "execute_result" 211 | }, 212 | { 213 | "name": "stdout", 214 | "output_type": "stream", 215 | "text": [ 216 | "In [7] used 0.008 MiB RAM in 0.012s, peaked 0.000 MiB above current, total RAM usage 66.113 MiB\n" 217 | ] 218 | } 219 | ], 220 | "source": [ 221 | "# How much memory take an int?\n", 222 | "# Beware: the size below will depend on whether you are using a 32-bit or 64-bit Python\n", 223 | "import sys\n", 224 | "sys.getsizeof(a[0])" 225 | ] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "metadata": {}, 230 | "source": [ 231 | "Ok. On 64-bits, that means that the int object allocates 8 bytes for the integer value, and 16 bytes for other metadata (Python object). But 24 is quite less than 32~35. Where this overhead comes from?\n", 232 | "\n", 233 | "Well, it turns out that the list structure needs additional pointers to reference the different components." 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "metadata": {}, 239 | "source": [ 240 | "![List diagram](http://www.brpreiss.com/books/opus7/html/img579.gif)" 241 | ] 242 | }, 243 | { 244 | "cell_type": "markdown", 245 | "metadata": {}, 246 | "source": [ 247 | "## memory_profiler" 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "[memory_profiler](https://pypi.python.org/pypi/memory_profiler) is a basic module for memory profiling that many others use (like the `ipython_memwatcher` above) and it interacts well with ipython, so it is worth to see how it works:" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": 8, 260 | "metadata": { 261 | "collapsed": false 262 | }, 263 | "outputs": [ 264 | { 265 | "name": "stdout", 266 | "output_type": "stream", 267 | "text": [ 268 | "In [8] used 0.070 MiB RAM in 0.007s, peaked 0.000 MiB above current, total RAM usage 66.184 MiB\n" 269 | ] 270 | } 271 | ], 272 | "source": [ 273 | "%load_ext memory_profiler" 274 | ] 275 | }, 276 | { 277 | "cell_type": "code", 278 | "execution_count": 9, 279 | "metadata": { 280 | "collapsed": false 281 | }, 282 | "outputs": [ 283 | { 284 | "name": "stdout", 285 | "output_type": "stream", 286 | "text": [ 287 | "peak memory: 105.27 MiB, increment: 39.07 MiB\n", 288 | "In [9] used 39.309 MiB RAM in 0.523s, peaked 0.000 MiB above current, total RAM usage 105.492 MiB\n" 289 | ] 290 | } 291 | ], 292 | "source": [ 293 | "# Use %memit magic command exposed by memory_profiler\n", 294 | "%memit b = [i for i in range(1000*1000)]" 295 | ] 296 | }, 297 | { 298 | "cell_type": "markdown", 299 | "metadata": {}, 300 | "source": [ 301 | "Please note that the `peak_memory` in this case is different than the `peaked_memory` reported by ipython_memwatcher package." 302 | ] 303 | }, 304 | { 305 | "cell_type": "markdown", 306 | "metadata": {}, 307 | "source": [ 308 | "## Guppy" 309 | ] 310 | }, 311 | { 312 | "cell_type": "markdown", 313 | "metadata": {}, 314 | "source": [ 315 | "Guppy is nice for having an overview of how different structures are using our memory:" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": 10, 321 | "metadata": { 322 | "collapsed": false 323 | }, 324 | "outputs": [ 325 | { 326 | "data": { 327 | "text/plain": [ 328 | "Partition of a set of 2146512 objects. Total size = 84880200 bytes.\n", 329 | " Index Count % Size % Cumulative % Kind (class / dict of class)\n", 330 | " 0 2003288 93 48078912 57 48078912 57 int\n", 331 | " 1 1816 0 16501632 19 64580544 76 list\n", 332 | " 2 69505 3 6527248 8 71107792 84 str\n", 333 | " 3 33715 2 2888640 3 73996432 87 tuple\n", 334 | " 4 1814 0 1797392 2 75793824 89 dict (no owner)\n", 335 | " 5 480 0 1405440 2 77199264 91 dict of module\n", 336 | " 6 8649 0 1107072 1 78306336 92 types.CodeType\n", 337 | " 7 8424 0 1010880 1 79317216 93 function\n", 338 | " 8 1021 0 920176 1 80237392 95 type\n", 339 | " 9 1021 0 835000 1 81072392 96 dict of type\n", 340 | "<657 more rows. Type e.g. '_.more' to view.>" 341 | ] 342 | }, 343 | "execution_count": 10, 344 | "metadata": {}, 345 | "output_type": "execute_result" 346 | }, 347 | { 348 | "name": "stdout", 349 | "output_type": "stream", 350 | "text": [ 351 | "In [10] used 68.152 MiB RAM in 13.666s, peaked 0.000 MiB above current, total RAM usage 173.645 MiB\n" 352 | ] 353 | } 354 | ], 355 | "source": [ 356 | "from guppy import hpy; hp=hpy()\n", 357 | "hp.heap()" 358 | ] 359 | }, 360 | { 361 | "cell_type": "code", 362 | "execution_count": 11, 363 | "metadata": { 364 | "collapsed": false 365 | }, 366 | "outputs": [ 367 | { 368 | "data": { 369 | "text/plain": [ 370 | "Partition of a set of 1146787 objects. Total size = 52765960 bytes.\n", 371 | " Index Count % Size % Cumulative % Kind (class / dict of class)\n", 372 | " 0 1003546 88 24085104 46 24085104 46 int\n", 373 | " 1 1813 0 8374952 16 32460056 62 list\n", 374 | " 2 69510 6 6527568 12 38987624 74 str\n", 375 | " 3 33716 3 2888744 5 41876368 79 tuple\n", 376 | " 4 1820 0 1800608 3 43676976 83 dict (no owner)\n", 377 | " 5 480 0 1405440 3 45082416 85 dict of module\n", 378 | " 6 8649 1 1107072 2 46189488 88 types.CodeType\n", 379 | " 7 8423 1 1010760 2 47200248 89 function\n", 380 | " 8 1021 0 920176 2 48120424 91 type\n", 381 | " 9 1021 0 835000 2 48955424 93 dict of type\n", 382 | "<653 more rows. Type e.g. '_.more' to view.>" 383 | ] 384 | }, 385 | "execution_count": 11, 386 | "metadata": {}, 387 | "output_type": "execute_result" 388 | }, 389 | { 390 | "name": "stdout", 391 | "output_type": "stream", 392 | "text": [ 393 | "In [11] used 12.312 MiB RAM in 2.690s, peaked 0.000 MiB above current, total RAM usage 185.957 MiB\n" 394 | ] 395 | } 396 | ], 397 | "source": [ 398 | "del b\n", 399 | "hp.heap()" 400 | ] 401 | }, 402 | { 403 | "cell_type": "code", 404 | "execution_count": 12, 405 | "metadata": { 406 | "collapsed": false 407 | }, 408 | "outputs": [ 409 | { 410 | "data": { 411 | "text/plain": [ 412 | "Partition of a set of 1 object. Total size = 8126536 bytes.\n", 413 | " Index Count % Size % Cumulative % Kind (class / dict of class)\n", 414 | " 0 1 100 8126536 100 8126536 100 list" 415 | ] 416 | }, 417 | "execution_count": 12, 418 | "metadata": {}, 419 | "output_type": "execute_result" 420 | }, 421 | { 422 | "name": "stdout", 423 | "output_type": "stream", 424 | "text": [ 425 | "In [12] used 0.016 MiB RAM in 0.014s, peaked 0.000 MiB above current, total RAM usage 185.973 MiB\n" 426 | ] 427 | } 428 | ], 429 | "source": [ 430 | "# Size of the list (beware, this does not include the contents!)\n", 431 | "hp.iso(a)" 432 | ] 433 | }, 434 | { 435 | "cell_type": "markdown", 436 | "metadata": { 437 | "collapsed": true 438 | }, 439 | "source": [ 440 | "## %time and %timeit" 441 | ] 442 | }, 443 | { 444 | "cell_type": "code", 445 | "execution_count": 13, 446 | "metadata": { 447 | "collapsed": false 448 | }, 449 | "outputs": [ 450 | { 451 | "name": "stdout", 452 | "output_type": "stream", 453 | "text": [ 454 | "CPU times: user 28 ms, sys: 4 ms, total: 32 ms\n", 455 | "Wall time: 30.5 ms\n", 456 | "In [13] used 0.012 MiB RAM in 0.037s, peaked 0.000 MiB above current, total RAM usage 185.984 MiB\n" 457 | ] 458 | } 459 | ], 460 | "source": [ 461 | "# IPython provides a magic command to see how much time a command takes to run\n", 462 | "%time asum = sum(a)" 463 | ] 464 | }, 465 | { 466 | "cell_type": "markdown", 467 | "metadata": {}, 468 | "source": [ 469 | "Note that `%time` offers quite detailed statistics on the time spent.\n", 470 | "\n", 471 | "Also, the time reported by MemoryWatcher has a typical overhead of 1~5 ms over the time reported by %time, so when the times to measure are about this order then it is better to rely on the %time (or %timeit below) values. " 472 | ] 473 | }, 474 | { 475 | "cell_type": "code", 476 | "execution_count": 14, 477 | "metadata": { 478 | "collapsed": false 479 | }, 480 | "outputs": [ 481 | { 482 | "name": "stdout", 483 | "output_type": "stream", 484 | "text": [ 485 | "10 loops, best of 3: 22 ms per loop\n", 486 | "In [14] used 0.023 MiB RAM in 0.946s, peaked 0.000 MiB above current, total RAM usage 186.008 MiB\n" 487 | ] 488 | } 489 | ], 490 | "source": [ 491 | "# We have another way to measure timings doing several loops and getting the mean\n", 492 | "%timeit bsum = sum(a)\n", 493 | "# However, one must notice that %timeit does not return the result of expressions" 494 | ] 495 | }, 496 | { 497 | "cell_type": "markdown", 498 | "metadata": {}, 499 | "source": [ 500 | "Interestingly, %timeit allows to retrieve the measured times in loops with the -o flag:" 501 | ] 502 | }, 503 | { 504 | "cell_type": "code", 505 | "execution_count": 15, 506 | "metadata": { 507 | "collapsed": false 508 | }, 509 | "outputs": [ 510 | { 511 | "name": "stdout", 512 | "output_type": "stream", 513 | "text": [ 514 | "10 loops, best of 3: 22.1 ms per loop\n", 515 | "[0.22296881675720215, 0.2211289405822754, 0.22654008865356445]\n", 516 | "0.0221128940582\n", 517 | "In [15] used 0.016 MiB RAM in 1.052s, peaked 0.000 MiB above current, total RAM usage 186.023 MiB\n" 518 | ] 519 | } 520 | ], 521 | "source": [ 522 | "t = %timeit -o sum(a)\n", 523 | "print(t.all_runs)\n", 524 | "print(t.best)" 525 | ] 526 | }, 527 | { 528 | "cell_type": "markdown", 529 | "metadata": {}, 530 | "source": [ 531 | "And one can specify the number of loops (-n) and the number of repetitions (-r):" 532 | ] 533 | }, 534 | { 535 | "cell_type": "code", 536 | "execution_count": 16, 537 | "metadata": { 538 | "collapsed": false 539 | }, 540 | "outputs": [ 541 | { 542 | "name": "stdout", 543 | "output_type": "stream", 544 | "text": [ 545 | "1 loop, best of 1: 29.6 ms per loop\n", 546 | "[0.029569149017333984]\n", 547 | "0.0295691490173\n", 548 | "In [16] used 0.008 MiB RAM in 0.049s, peaked 0.000 MiB above current, total RAM usage 186.031 MiB\n" 549 | ] 550 | } 551 | ], 552 | "source": [ 553 | "t = %timeit -r1 -n1 -o sum(a)\n", 554 | "print(t.all_runs)\n", 555 | "print(t.best)" 556 | ] 557 | }, 558 | { 559 | "cell_type": "markdown", 560 | "metadata": {}, 561 | "source": [ 562 | "### Exercise 1\n", 563 | "\n", 564 | "Provided a dictionary like:\n", 565 | "\n", 566 | "```\n", 567 | "d = dict((\"key: %i\"%i, i*2) for i in a)\n", 568 | "```\n", 569 | "\n", 570 | "Try to guess how much RAM it uses using the techniques introduced above.\n", 571 | "\n", 572 | "Why do you think it takes more space than a list?\n", 573 | "\n", 574 | "*Hint*: Every entry in a dictionary has pointers to two objects: key and value. " 575 | ] 576 | }, 577 | { 578 | "cell_type": "markdown", 579 | "metadata": { 580 | "collapsed": true 581 | }, 582 | "source": [ 583 | "## Solution" 584 | ] 585 | }, 586 | { 587 | "cell_type": "code", 588 | "execution_count": 20, 589 | "metadata": { 590 | "collapsed": false 591 | }, 592 | "outputs": [ 593 | { 594 | "name": "stdout", 595 | "output_type": "stream", 596 | "text": [ 597 | "In [20] used 103.508 MiB RAM in 2.975s, peaked 0.000 MiB above current, total RAM usage 399.398 MiB\n" 598 | ] 599 | } 600 | ], 601 | "source": [ 602 | "d = dict((\"key: %i\"%i, i*2) for i in a)" 603 | ] 604 | }, 605 | { 606 | "cell_type": "code", 607 | "execution_count": 18, 608 | "metadata": { 609 | "collapsed": false 610 | }, 611 | "outputs": [ 612 | { 613 | "data": { 614 | "text/plain": [ 615 | "72" 616 | ] 617 | }, 618 | "execution_count": 18, 619 | "metadata": {}, 620 | "output_type": "execute_result" 621 | }, 622 | { 623 | "name": "stdout", 624 | "output_type": "stream", 625 | "text": [ 626 | "In [18] used 0.008 MiB RAM in 0.008s, peaked 0.000 MiB above current, total RAM usage 295.887 MiB\n" 627 | ] 628 | } 629 | ], 630 | "source": [ 631 | "# Compute the size of key + value\n", 632 | "sys.getsizeof(\"key: 100000\") + sys.getsizeof(1)" 633 | ] 634 | }, 635 | { 636 | "cell_type": "code", 637 | "execution_count": 19, 638 | "metadata": { 639 | "collapsed": false 640 | }, 641 | "outputs": [ 642 | { 643 | "data": { 644 | "text/plain": [ 645 | "Partition of a set of 1 object. Total size = 50331928 bytes.\n", 646 | " Index Count % Size % Cumulative % Kind (class / dict of class)\n", 647 | " 0 1 100 50331928 100 50331928 100 dict (no owner)" 648 | ] 649 | }, 650 | "execution_count": 19, 651 | "metadata": {}, 652 | "output_type": "execute_result" 653 | }, 654 | { 655 | "name": "stdout", 656 | "output_type": "stream", 657 | "text": [ 658 | "In [19] used 0.004 MiB RAM in 2.018s, peaked 0.000 MiB above current, total RAM usage 295.891 MiB\n" 659 | ] 660 | } 661 | ], 662 | "source": [ 663 | "# Compute the size of the dict struct (using guppy here)\n", 664 | "hp.iso(d)" 665 | ] 666 | }, 667 | { 668 | "cell_type": "markdown", 669 | "metadata": {}, 670 | "source": [ 671 | "So, guppy is telling us that just the dictionary structure is taking ~50 MB, whereas the contents alone are taking ~70MB, so we should have expected the dictionary to consume ~120 MB. However, our `MemWatcher` instance is reporting just ~100 MB. Take away lesson: measuring memory consumption in Python is tricky but using proper tools we can still get valuable hints!" 672 | ] 673 | }, 674 | { 675 | "cell_type": "code", 676 | "execution_count": null, 677 | "metadata": { 678 | "collapsed": true 679 | }, 680 | "outputs": [], 681 | "source": [] 682 | } 683 | ], 684 | "metadata": { 685 | "kernelspec": { 686 | "display_name": "Python 2", 687 | "language": "python", 688 | "name": "python2" 689 | }, 690 | "language_info": { 691 | "codemirror_mode": { 692 | "name": "ipython", 693 | "version": 2 694 | }, 695 | "file_extension": ".py", 696 | "mimetype": "text/x-python", 697 | "name": "python", 698 | "nbconvert_exporter": "python", 699 | "pygments_lexer": "ipython2", 700 | "version": "2.7.11" 701 | } 702 | }, 703 | "nbformat": 4, 704 | "nbformat_minor": 0 705 | } 706 | -------------------------------------------------------------------------------- /2-A-Reduction-Tale.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# A Reduction Tale" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "> Objectives:\n", 15 | "> * Compare operations taking place in different data containers\n", 16 | "> * Compare sizes for these data containers\n", 17 | "> * Help deciding when it is best to use a container or another" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "Let's suppose that we are going to need reductions a lot and we want to choose the best container for performing them. First, let's start by activating our MemWatcher agent:" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 1, 30 | "metadata": { 31 | "collapsed": false 32 | }, 33 | "outputs": [ 34 | { 35 | "name": "stdout", 36 | "output_type": "stream", 37 | "text": [ 38 | "In [1] used 0.031 MiB RAM in 0.002s, peaked 0.000 MiB above current, total RAM usage 34.965 MiB\n" 39 | ] 40 | } 41 | ], 42 | "source": [ 43 | "from ipython_memwatcher import MemWatcher\n", 44 | "mw = MemWatcher()\n", 45 | "mw.start_watching_memory()" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "and choose a different container for the data that we want to reduce, starting with a list:" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "## Regular lists" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 2, 65 | "metadata": { 66 | "collapsed": false, 67 | "scrolled": true 68 | }, 69 | "outputs": [ 70 | { 71 | "name": "stdout", 72 | "output_type": "stream", 73 | "text": [ 74 | "In [2] used 545.145 MiB RAM in 8.569s, peaked 70.184 MiB above current, total RAM usage 580.109 MiB\n" 75 | ] 76 | } 77 | ], 78 | "source": [ 79 | "a = [float(i) for i in range(10*1000*1000)]" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "metadata": {}, 85 | "source": [ 86 | "Now, proceed with a simple reduction (sum):" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 3, 92 | "metadata": { 93 | "collapsed": false 94 | }, 95 | "outputs": [ 96 | { 97 | "name": "stdout", 98 | "output_type": "stream", 99 | "text": [ 100 | "1 loop, best of 3: 231 ms per loop\n", 101 | "In [3] used 0.062 MiB RAM in 0.957s, peaked 0.000 MiB above current, total RAM usage 580.172 MiB\n" 102 | ] 103 | } 104 | ], 105 | "source": [ 106 | "t = %timeit -o sum(a)" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "which, in MFLOPS (Mega-FloatingPointOps-Per-Second) is:" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": 4, 119 | "metadata": { 120 | "collapsed": false 121 | }, 122 | "outputs": [ 123 | { 124 | "name": "stdout", 125 | "output_type": "stream", 126 | "text": [ 127 | "('MFLOPS:', 43.3)\n", 128 | "In [4] used 0.008 MiB RAM in 0.003s, peaked 0.000 MiB above current, total RAM usage 580.180 MiB\n" 129 | ] 130 | } 131 | ], 132 | "source": [ 133 | "print(\"MFLOPS:\", round((len(a) / t.best / 1e6), 1))" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "metadata": {}, 139 | "source": [ 140 | "Ok, so that seems fast, but we don't have other references to compare with. In addition, a list is not the best kind of container in terms of space consumption. So let's try now a container that seems quite optimal in terms of memory savings." 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "metadata": {}, 146 | "source": [ 147 | "## NumPy" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": 10, 153 | "metadata": { 154 | "collapsed": false, 155 | "scrolled": true 156 | }, 157 | "outputs": [ 158 | { 159 | "name": "stdout", 160 | "output_type": "stream", 161 | "text": [ 162 | "In [10] used 0.000 MiB RAM in 0.003s, peaked 0.000 MiB above current, total RAM usage 672.172 MiB\n" 163 | ] 164 | } 165 | ], 166 | "source": [ 167 | "import numpy as np" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 11, 173 | "metadata": { 174 | "collapsed": false 175 | }, 176 | "outputs": [ 177 | { 178 | "name": "stdout", 179 | "output_type": "stream", 180 | "text": [ 181 | "In [11] used 0.000 MiB RAM in 1.102s, peaked 0.000 MiB above current, total RAM usage 672.172 MiB\n" 182 | ] 183 | } 184 | ], 185 | "source": [ 186 | "na = np.array(a, dtype=np.float64)" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": 7, 192 | "metadata": { 193 | "collapsed": false 194 | }, 195 | "outputs": [ 196 | { 197 | "name": "stdout", 198 | "output_type": "stream", 199 | "text": [ 200 | "('SIZE:', 76.294)\n", 201 | "In [7] used 0.004 MiB RAM in 0.004s, peaked 0.000 MiB above current, total RAM usage 672.148 MiB\n" 202 | ] 203 | } 204 | ], 205 | "source": [ 206 | "print(\"SIZE:\", round((na.size * na.itemsize) / 2**20., 3))" 207 | ] 208 | }, 209 | { 210 | "cell_type": "markdown", 211 | "metadata": {}, 212 | "source": [ 213 | "We see that, with 8 bytes/element, NumPy is a very efficient container." 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": 8, 219 | "metadata": { 220 | "collapsed": false 221 | }, 222 | "outputs": [ 223 | { 224 | "name": "stdout", 225 | "output_type": "stream", 226 | "text": [ 227 | "1 loop, best of 3: 3.52 s per loop\n", 228 | "In [8] used 0.020 MiB RAM in 14.138s, peaked 0.000 MiB above current, total RAM usage 672.168 MiB\n" 229 | ] 230 | } 231 | ], 232 | "source": [ 233 | "t = %timeit -o sum(na)" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": 9, 239 | "metadata": { 240 | "collapsed": false 241 | }, 242 | "outputs": [ 243 | { 244 | "name": "stdout", 245 | "output_type": "stream", 246 | "text": [ 247 | "('MFLOPS:', 2.842)\n", 248 | "In [9] used 0.004 MiB RAM in 0.005s, peaked 0.000 MiB above current, total RAM usage 672.172 MiB\n" 249 | ] 250 | } 251 | ], 252 | "source": [ 253 | "print(\"MFLOPS:\", round(len(a) / t.best / 1e6, 3))" 254 | ] 255 | }, 256 | { 257 | "cell_type": "markdown", 258 | "metadata": {}, 259 | "source": [ 260 | "### Exercise" 261 | ] 262 | }, 263 | { 264 | "cell_type": "markdown", 265 | "metadata": {}, 266 | "source": [ 267 | "The performance for NumPy is several times slower than the computation with the list. Why so?\n", 268 | "\n", 269 | "*Hint: * We are using sum() which is a Python function." 270 | ] 271 | }, 272 | { 273 | "cell_type": "markdown", 274 | "metadata": {}, 275 | "source": [ 276 | "### Solution" 277 | ] 278 | }, 279 | { 280 | "cell_type": "markdown", 281 | "metadata": {}, 282 | "source": [ 283 | "NumPy has a lot of overhead in producing a Python integer for every element in the array for feeding it to the sum().\n", 284 | "\n", 285 | "*Hint:* Use internal NumPy methods (ufuncs) when possible." 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 12, 291 | "metadata": { 292 | "collapsed": false 293 | }, 294 | "outputs": [ 295 | { 296 | "name": "stdout", 297 | "output_type": "stream", 298 | "text": [ 299 | "10 loops, best of 3: 19 ms per loop\n", 300 | "In [12] used 0.086 MiB RAM in 0.812s, peaked 0.000 MiB above current, total RAM usage 672.258 MiB\n" 301 | ] 302 | } 303 | ], 304 | "source": [ 305 | "t = %timeit -o na.sum()" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": 13, 311 | "metadata": { 312 | "collapsed": false 313 | }, 314 | "outputs": [ 315 | { 316 | "name": "stdout", 317 | "output_type": "stream", 318 | "text": [ 319 | "('FLOPS:', 525.329)\n", 320 | "In [13] used 0.004 MiB RAM in 0.004s, peaked 0.000 MiB above current, total RAM usage 672.262 MiB\n" 321 | ] 322 | } 323 | ], 324 | "source": [ 325 | "print(\"FLOPS:\", round(len(a) / t.best / 1e6, 3))" 326 | ] 327 | }, 328 | { 329 | "cell_type": "markdown", 330 | "metadata": {}, 331 | "source": [ 332 | "This is about 100x the speed of sum() on a Python list and it is also pretty optimal in terms of both execution time and space consumed." 333 | ] 334 | }, 335 | { 336 | "cell_type": "markdown", 337 | "metadata": {}, 338 | "source": [ 339 | "## Exercise\n", 340 | "\n", 341 | "The speed in the above reduction is limited by memory speed, not CPU speed. Could you provide a hint on the maximum memory speed that supports your laptop?" 342 | ] 343 | }, 344 | { 345 | "cell_type": "markdown", 346 | "metadata": {}, 347 | "source": [ 348 | "#### Solution" 349 | ] 350 | }, 351 | { 352 | "cell_type": "code", 353 | "execution_count": 14, 354 | "metadata": { 355 | "collapsed": false 356 | }, 357 | "outputs": [ 358 | { 359 | "data": { 360 | "text/plain": [ 361 | "3.914004621656657" 362 | ] 363 | }, 364 | "execution_count": 14, 365 | "metadata": {}, 366 | "output_type": "execute_result" 367 | }, 368 | { 369 | "name": "stdout", 370 | "output_type": "stream", 371 | "text": [ 372 | "In [14] used 0.090 MiB RAM in 0.049s, peaked 0.000 MiB above current, total RAM usage 672.352 MiB\n" 373 | ] 374 | } 375 | ], 376 | "source": [ 377 | "# This is an easy one. Just divide the size of the dataset by the time that takes the reduction\n", 378 | "(na.size * na.itemsize) / t.best / 2**30" 379 | ] 380 | }, 381 | { 382 | "cell_type": "markdown", 383 | "metadata": {}, 384 | "source": [ 385 | "So, in this case the memory bandwidth is close to 10 GB/s." 386 | ] 387 | }, 388 | { 389 | "cell_type": "markdown", 390 | "metadata": {}, 391 | "source": [ 392 | "## Using compressed in-memory containers with bcolz" 393 | ] 394 | }, 395 | { 396 | "cell_type": "markdown", 397 | "metadata": {}, 398 | "source": [ 399 | "But let us suppose that we have really big data to process in our laptop and want to see if we can store our data in less space. Enter compression:" 400 | ] 401 | }, 402 | { 403 | "cell_type": "code", 404 | "execution_count": 15, 405 | "metadata": { 406 | "collapsed": false 407 | }, 408 | "outputs": [ 409 | { 410 | "name": "stdout", 411 | "output_type": "stream", 412 | "text": [ 413 | "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n", 414 | "bcolz version: 1.0.0rc2\n", 415 | "NumPy version: 1.10.4\n", 416 | "Blosc version: 1.8.0 ($Date:: 2016-03-31 #$)\n", 417 | "Blosc compressors: ['blosclz', 'lz4', 'lz4hc', 'snappy', 'zlib']\n", 418 | "Numexpr version: 2.5.1\n", 419 | "Python version: 2.7.11 |Continuum Analytics, Inc.| (default, Dec 6 2015, 18:08:32) \n", 420 | "[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]\n", 421 | "Platform: linux2-x86_64\n", 422 | "Byte-ordering: little\n", 423 | "Detected cores: 4\n", 424 | "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n" 425 | ] 426 | }, 427 | { 428 | "data": { 429 | "text/plain": [ 430 | "4" 431 | ] 432 | }, 433 | "execution_count": 15, 434 | "metadata": {}, 435 | "output_type": "execute_result" 436 | }, 437 | { 438 | "name": "stdout", 439 | "output_type": "stream", 440 | "text": [ 441 | "In [15] used 22.062 MiB RAM in 1.776s, peaked 0.000 MiB above current, total RAM usage 694.414 MiB\n" 442 | ] 443 | } 444 | ], 445 | "source": [ 446 | "import bcolz\n", 447 | "bcolz.print_versions()\n", 448 | "bcolz.defaults.cparams['cname'] = 'blosclz'\n", 449 | "bcolz.defaults.cparams['clevel'] = 9\n", 450 | "bcolz.defaults.cparams['shuffle'] = bcolz.SHUFFLE\n", 451 | "bcolz.set_nthreads(4)" 452 | ] 453 | }, 454 | { 455 | "cell_type": "code", 456 | "execution_count": 16, 457 | "metadata": { 458 | "collapsed": false 459 | }, 460 | "outputs": [ 461 | { 462 | "name": "stdout", 463 | "output_type": "stream", 464 | "text": [ 465 | "In [16] used 0.309 MiB RAM in 0.103s, peaked 0.000 MiB above current, total RAM usage 694.723 MiB\n" 466 | ] 467 | } 468 | ], 469 | "source": [ 470 | "ca = bcolz.carray(na)" 471 | ] 472 | }, 473 | { 474 | "cell_type": "code", 475 | "execution_count": 17, 476 | "metadata": { 477 | "collapsed": false 478 | }, 479 | "outputs": [ 480 | { 481 | "name": "stdout", 482 | "output_type": "stream", 483 | "text": [ 484 | "('mem_used:', 0.30859375)\n", 485 | "In [17] used 0.008 MiB RAM in 0.004s, peaked 0.000 MiB above current, total RAM usage 694.730 MiB\n" 486 | ] 487 | } 488 | ], 489 | "source": [ 490 | "print(\"mem_used:\", mw.measurements.memory_delta)" 491 | ] 492 | }, 493 | { 494 | "cell_type": "markdown", 495 | "metadata": {}, 496 | "source": [ 497 | "Ok, this time the amount of memory used seems much lower. Also, bcolz containers can provide an estimation on how much memory they are taking; let's have a look:" 498 | ] 499 | }, 500 | { 501 | "cell_type": "code", 502 | "execution_count": 18, 503 | "metadata": { 504 | "collapsed": false 505 | }, 506 | "outputs": [ 507 | { 508 | "data": { 509 | "text/plain": [ 510 | "carray((10000000,), float64)\n", 511 | " nbytes: 76.29 MB; cbytes: 1.01 MB; ratio: 75.31\n", 512 | " cparams := cparams(clevel=9, shuffle=1, cname='blosclz')\n", 513 | "[ 0.00000000e+00 1.00000000e+00 2.00000000e+00 ..., 9.99999700e+06\n", 514 | " 9.99999800e+06 9.99999900e+06]" 515 | ] 516 | }, 517 | "execution_count": 18, 518 | "metadata": {}, 519 | "output_type": "execute_result" 520 | }, 521 | { 522 | "name": "stdout", 523 | "output_type": "stream", 524 | "text": [ 525 | "In [18] used 0.016 MiB RAM in 0.016s, peaked 0.000 MiB above current, total RAM usage 694.746 MiB\n" 526 | ] 527 | } 528 | ], 529 | "source": [ 530 | "ca" 531 | ] 532 | }, 533 | { 534 | "cell_type": "markdown", 535 | "metadata": {}, 536 | "source": [ 537 | "In this case we see that bcolz estimation is reasonably close to `ipython_memwatcher` measurements. Let's have a look at the speed of the reduction:" 538 | ] 539 | }, 540 | { 541 | "cell_type": "code", 542 | "execution_count": 19, 543 | "metadata": { 544 | "collapsed": false 545 | }, 546 | "outputs": [ 547 | { 548 | "name": "stdout", 549 | "output_type": "stream", 550 | "text": [ 551 | "10 loops, best of 3: 79.8 ms per loop\n", 552 | "('MFLOPS:', 125.344)\n", 553 | "In [19] used 0.020 MiB RAM in 3.307s, peaked 0.000 MiB above current, total RAM usage 694.766 MiB\n" 554 | ] 555 | } 556 | ], 557 | "source": [ 558 | "t = %timeit -o ca.sum()\n", 559 | "print(\"MFLOPS:\", round(len(a) / t.best / 1e6, 3))" 560 | ] 561 | }, 562 | { 563 | "cell_type": "markdown", 564 | "metadata": {}, 565 | "source": [ 566 | "This is around 2~5x slower (depending on the machine) than a regular NumPy array, but the size of the array is an impressive 75x smaller. But is compression the only responsible of the overhead? Let's investigate a bit further." 567 | ] 568 | }, 569 | { 570 | "cell_type": "markdown", 571 | "metadata": {}, 572 | "source": [ 573 | "## Using uncompressed containers with bcolz" 574 | ] 575 | }, 576 | { 577 | "cell_type": "markdown", 578 | "metadata": { 579 | "collapsed": true 580 | }, 581 | "source": [ 582 | "In order to see if this is because of the compression overhead, let's use an uncompressed array:" 583 | ] 584 | }, 585 | { 586 | "cell_type": "code", 587 | "execution_count": 20, 588 | "metadata": { 589 | "collapsed": false 590 | }, 591 | "outputs": [ 592 | { 593 | "name": "stdout", 594 | "output_type": "stream", 595 | "text": [ 596 | "In [20] used 0.039 MiB RAM in 1.154s, peaked 0.000 MiB above current, total RAM usage 694.805 MiB\n" 597 | ] 598 | } 599 | ], 600 | "source": [ 601 | "cau = bcolz.carray(a, cparams=bcolz.cparams(clevel=0))" 602 | ] 603 | }, 604 | { 605 | "cell_type": "code", 606 | "execution_count": 21, 607 | "metadata": { 608 | "collapsed": false 609 | }, 610 | "outputs": [ 611 | { 612 | "data": { 613 | "text/plain": [ 614 | "carray((10000000,), float64)\n", 615 | " nbytes: 76.29 MB; cbytes: 76.52 MB; ratio: 1.00\n", 616 | " cparams := cparams(clevel=0, shuffle=1, cname='blosclz')\n", 617 | "[ 0.00000000e+00 1.00000000e+00 2.00000000e+00 ..., 9.99999700e+06\n", 618 | " 9.99999800e+06 9.99999900e+06]" 619 | ] 620 | }, 621 | "execution_count": 21, 622 | "metadata": {}, 623 | "output_type": "execute_result" 624 | }, 625 | { 626 | "name": "stdout", 627 | "output_type": "stream", 628 | "text": [ 629 | "In [21] used 0.004 MiB RAM in 0.011s, peaked 0.000 MiB above current, total RAM usage 694.809 MiB\n" 630 | ] 631 | } 632 | ], 633 | "source": [ 634 | "cau" 635 | ] 636 | }, 637 | { 638 | "cell_type": "code", 639 | "execution_count": 22, 640 | "metadata": { 641 | "collapsed": false 642 | }, 643 | "outputs": [ 644 | { 645 | "name": "stdout", 646 | "output_type": "stream", 647 | "text": [ 648 | "10 loops, best of 3: 52 ms per loop\n", 649 | "('MFLOPS:', 192.383)\n", 650 | "In [22] used 0.008 MiB RAM in 2.206s, peaked 0.000 MiB above current, total RAM usage 694.816 MiB\n" 651 | ] 652 | } 653 | ], 654 | "source": [ 655 | "t = %timeit -o cau.sum()\n", 656 | "print(\"MFLOPS:\", round(len(a) / t.best / 1e6, 3))" 657 | ] 658 | }, 659 | { 660 | "cell_type": "markdown", 661 | "metadata": {}, 662 | "source": [ 663 | "As we can see, the times with an uncompressed `carray` are very close to a compressed one, so compressing is not the only source of the overhead (and it is very minor in fact). The actual source of the difference is the memory layout of the different containers (bcolz layout is a bit more complex than NumPy).\n", 664 | "\n", 665 | "So, bcolz allows to use compressed in-memory data containers at the cost of some performance (compared with NumPy). But the performance overhead is rather small, and sometimes you prefer to keep more data in-memory.\n", 666 | "\n", 667 | "On another hand, in the next notebooks we are going to see that bcolz can be pretty close to NumPy, performance wise, in some scenarios." 668 | ] 669 | } 670 | ], 671 | "metadata": { 672 | "kernelspec": { 673 | "display_name": "Python 2", 674 | "language": "python", 675 | "name": "python2" 676 | }, 677 | "language_info": { 678 | "codemirror_mode": { 679 | "name": "ipython", 680 | "version": 2 681 | }, 682 | "file_extension": ".py", 683 | "mimetype": "text/x-python", 684 | "name": "python", 685 | "nbconvert_exporter": "python", 686 | "pygments_lexer": "ipython2", 687 | "version": "2.7.11" 688 | } 689 | }, 690 | "nbformat": 4, 691 | "nbformat_minor": 0 692 | } 693 | -------------------------------------------------------------------------------- /3-In-Memory-Tables.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Querying tables" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "> Objectives:\n", 15 | "> * Compare queries of tabular data for **in-memory** containers\n", 16 | "> * Compare sizes and times for those" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "metadata": { 23 | "collapsed": false 24 | }, 25 | "outputs": [ 26 | { 27 | "name": "stdout", 28 | "output_type": "stream", 29 | "text": [ 30 | "In [1] used 0.004 MiB RAM in 0.000s, peaked 0.000 MiB above current, total RAM usage 32.367 MiB\n" 31 | ] 32 | } 33 | ], 34 | "source": [ 35 | "from ipython_memwatcher import MemWatcher\n", 36 | "mw = MemWatcher()\n", 37 | "mw.start_watching_memory()" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "metadata": { 44 | "collapsed": false 45 | }, 46 | "outputs": [ 47 | { 48 | "name": "stdout", 49 | "output_type": "stream", 50 | "text": [ 51 | "In [2] used 0.027 MiB RAM in 0.001s, peaked 0.000 MiB above current, total RAM usage 32.395 MiB\n" 52 | ] 53 | } 54 | ], 55 | "source": [ 56 | "import os\n", 57 | "dset = 'movielens-1m'\n", 58 | "fdata = os.path.join(dset, 'ratings.dat.gz')\n", 59 | "fitem = os.path.join(dset, 'movies.dat')" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 3, 65 | "metadata": { 66 | "collapsed": false 67 | }, 68 | "outputs": [ 69 | { 70 | "name": "stdout", 71 | "output_type": "stream", 72 | "text": [ 73 | "In [3] used 69.555 MiB RAM in 0.447s, peaked 2.562 MiB above current, total RAM usage 101.949 MiB\n" 74 | ] 75 | } 76 | ], 77 | "source": [ 78 | "import pandas as pd\n", 79 | "# pass in column names for each CSV\n", 80 | "r_cols = ['user_id', 'movie_id', 'rating', 'unix_timestamp']\n", 81 | "ratings = pd.read_csv(fdata, sep=';', names=r_cols, compression='gzip')\n", 82 | "\n", 83 | "m_cols = ['movie_id', 'title', 'genres']\n", 84 | "movies = pd.read_csv(fitem, sep=';', names=m_cols,\n", 85 | " dtype={'title': object, 'genres': object})" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 4, 91 | "metadata": { 92 | "collapsed": false 93 | }, 94 | "outputs": [ 95 | { 96 | "name": "stdout", 97 | "output_type": "stream", 98 | "text": [ 99 | "In [4] used 54.469 MiB RAM in 0.133s, peaked 0.000 MiB above current, total RAM usage 156.418 MiB\n" 100 | ] 101 | } 102 | ], 103 | "source": [ 104 | "lens = pd.merge(movies, ratings)" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 5, 110 | "metadata": { 111 | "collapsed": false 112 | }, 113 | "outputs": [ 114 | { 115 | "data": { 116 | "text/plain": [ 117 | "54.46875" 118 | ] 119 | }, 120 | "execution_count": 5, 121 | "metadata": {}, 122 | "output_type": "execute_result" 123 | }, 124 | { 125 | "name": "stdout", 126 | "output_type": "stream", 127 | "text": [ 128 | "In [5] used 0.047 MiB RAM in 0.010s, peaked 0.000 MiB above current, total RAM usage 156.465 MiB\n" 129 | ] 130 | } 131 | ], 132 | "source": [ 133 | "size_pandas = mw.measurements.memory_delta\n", 134 | "size_pandas" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": 6, 140 | "metadata": { 141 | "collapsed": false 142 | }, 143 | "outputs": [ 144 | { 145 | "name": "stdout", 146 | "output_type": "stream", 147 | "text": [ 148 | "Index 8001672\n", 149 | "movie_id 8001672\n", 150 | "title 8001672\n", 151 | "genres 8001672\n", 152 | "user_id 8001672\n", 153 | "rating 8001672\n", 154 | "unix_timestamp 8001672\n", 155 | "dtype: int64\n", 156 | "In [6] used 0.152 MiB RAM in 0.004s, peaked 0.000 MiB above current, total RAM usage 156.617 MiB\n" 157 | ] 158 | } 159 | ], 160 | "source": [ 161 | "# pandas also comes with its own tool for getting memory usage\n", 162 | "print(lens.memory_usage())" 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "metadata": {}, 168 | "source": [ 169 | "### Exercise\n", 170 | "\n", 171 | "Why do you think that the size is the same for every column in the dataframe?\n", 172 | "\n", 173 | "*Hint:* Pandas stores the string columns in NumPy containers with 'object' dtype." 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": 7, 179 | "metadata": { 180 | "collapsed": false 181 | }, 182 | "outputs": [ 183 | { 184 | "name": "stdout", 185 | "output_type": "stream", 186 | "text": [ 187 | " movie_id title genres user_id rating \\\n", 188 | "0 1 Toy Story (1995) Animation|Children's|Comedy 1 5 \n", 189 | "1 1 Toy Story (1995) Animation|Children's|Comedy 6 4 \n", 190 | "2 1 Toy Story (1995) Animation|Children's|Comedy 8 4 \n", 191 | "3 1 Toy Story (1995) Animation|Children's|Comedy 9 5 \n", 192 | "4 1 Toy Story (1995) Animation|Children's|Comedy 10 5 \n", 193 | "5 1 Toy Story (1995) Animation|Children's|Comedy 18 4 \n", 194 | "6 1 Toy Story (1995) Animation|Children's|Comedy 19 5 \n", 195 | "7 1 Toy Story (1995) Animation|Children's|Comedy 21 3 \n", 196 | "8 1 Toy Story (1995) Animation|Children's|Comedy 23 4 \n", 197 | "9 1 Toy Story (1995) Animation|Children's|Comedy 26 3 \n", 198 | "\n", 199 | " unix_timestamp \n", 200 | "0 978824268 \n", 201 | "1 978237008 \n", 202 | "2 978233496 \n", 203 | "3 978225952 \n", 204 | "4 978226474 \n", 205 | "5 978154768 \n", 206 | "6 978555994 \n", 207 | "7 978139347 \n", 208 | "8 978463614 \n", 209 | "9 978130703 \n", 210 | "\n", 211 | "Int64Index: 1000209 entries, 0 to 1000208\n", 212 | "Data columns (total 6 columns):\n", 213 | "movie_id 1000209 non-null int64\n", 214 | "title 1000209 non-null object\n", 215 | "genres 1000209 non-null object\n", 216 | "user_id 1000209 non-null int64\n", 217 | "rating 1000209 non-null int64\n", 218 | "unix_timestamp 1000209 non-null int64\n", 219 | "dtypes: int64(4), object(2)\n", 220 | "memory usage: 53.4+ MB\n", 221 | "In [7] used 24.805 MiB RAM in 0.154s, peaked 0.000 MiB above current, total RAM usage 181.422 MiB\n" 222 | ] 223 | } 224 | ], 225 | "source": [ 226 | "# See how pandas dataframe looks like\n", 227 | "print(lens[:10])\n", 228 | "lens.info()" 229 | ] 230 | }, 231 | { 232 | "cell_type": "markdown", 233 | "metadata": { 234 | "collapsed": true 235 | }, 236 | "source": [ 237 | "### Solution" 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": 8, 243 | "metadata": { 244 | "collapsed": false 245 | }, 246 | "outputs": [ 247 | { 248 | "name": "stdout", 249 | "output_type": "stream", 250 | "text": [ 251 | "Index 8001672\n", 252 | "movie_id 8001672\n", 253 | "title 68344698\n", 254 | "genres 59662643\n", 255 | "user_id 8001672\n", 256 | "rating 8001672\n", 257 | "unix_timestamp 8001672\n", 258 | "dtype: int64\n", 259 | "In [8] used 0.047 MiB RAM in 0.085s, peaked 0.000 MiB above current, total RAM usage 181.469 MiB\n" 260 | ] 261 | } 262 | ], 263 | "source": [ 264 | "# Use deep=True for including the size of the object types\n", 265 | "print(lens.memory_usage(deep=True))" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": 9, 271 | "metadata": { 272 | "collapsed": false 273 | }, 274 | "outputs": [ 275 | { 276 | "data": { 277 | "text/plain": [ 278 | "160.23225879669189" 279 | ] 280 | }, 281 | "execution_count": 9, 282 | "metadata": {}, 283 | "output_type": "execute_result" 284 | }, 285 | { 286 | "name": "stdout", 287 | "output_type": "stream", 288 | "text": [ 289 | "In [9] used 0.012 MiB RAM in 0.093s, peaked 0.000 MiB above current, total RAM usage 181.480 MiB\n" 290 | ] 291 | } 292 | ], 293 | "source": [ 294 | "# For a more realistic memory usage, we are going to use deep=True\n", 295 | "size_pandas2 = lens.memory_usage(deep=True).sum() / 2**20.\n", 296 | "size_pandas2" 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": 10, 302 | "metadata": { 303 | "collapsed": false 304 | }, 305 | "outputs": [ 306 | { 307 | "name": "stdout", 308 | "output_type": "stream", 309 | "text": [ 310 | "10 loops, best of 3: 30 ms per loop\n" 311 | ] 312 | }, 313 | { 314 | "data": { 315 | "text/plain": [ 316 | "5121 75\n", 317 | "5164 3842\n", 318 | "5187 6031\n", 319 | "Name: user_id, dtype: int64" 320 | ] 321 | }, 322 | "execution_count": 10, 323 | "metadata": {}, 324 | "output_type": "execute_result" 325 | }, 326 | { 327 | "name": "stdout", 328 | "output_type": "stream", 329 | "text": [ 330 | "In [10] used 0.191 MiB RAM in 1.287s, peaked 0.000 MiB above current, total RAM usage 181.672 MiB\n" 331 | ] 332 | } 333 | ], 334 | "source": [ 335 | "# Let's see how fast a query can be\n", 336 | "result = lens.query(\"(title == 'Tom and Huck (1995)') & (rating == 5)\")['user_id']\n", 337 | "t = %timeit -o lens.query(\"(title == 'Tom and Huck (1995)') & (rating == 5)\")['user_id']\n", 338 | "result" 339 | ] 340 | }, 341 | { 342 | "cell_type": "code", 343 | "execution_count": 11, 344 | "metadata": { 345 | "collapsed": false 346 | }, 347 | "outputs": [ 348 | { 349 | "name": "stdout", 350 | "output_type": "stream", 351 | "text": [ 352 | "In [11] used 0.008 MiB RAM in 0.002s, peaked 0.000 MiB above current, total RAM usage 181.680 MiB\n" 353 | ] 354 | } 355 | ], 356 | "source": [ 357 | "# Sotre it for future reference\n", 358 | "qtime_pandas = t.best" 359 | ] 360 | }, 361 | { 362 | "cell_type": "markdown", 363 | "metadata": {}, 364 | "source": [ 365 | "## Use a compressed in-memory container via `bcolz`" 366 | ] 367 | }, 368 | { 369 | "cell_type": "code", 370 | "execution_count": 12, 371 | "metadata": { 372 | "collapsed": false, 373 | "scrolled": true 374 | }, 375 | "outputs": [ 376 | { 377 | "name": "stdout", 378 | "output_type": "stream", 379 | "text": [ 380 | "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n", 381 | "bcolz version: 1.0.1.dev99\n", 382 | "bcolz git info: 1.0.0-99-gcbe1434\n", 383 | "NumPy version: 1.11.0\n", 384 | "Blosc version: 1.9.1 ($Date:: 2016-05-20 #$)\n", 385 | "Blosc compressors: ['blosclz', 'lz4', 'lz4hc', 'snappy', 'zlib']\n", 386 | "Numexpr version: 2.6.1.dev0\n", 387 | "Dask version: 0.9.0\n", 388 | "Python version: 2.7.11 |Continuum Analytics, Inc.| (default, Dec 6 2015, 18:08:32) \n", 389 | "[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]\n", 390 | "Platform: linux2-x86_64\n", 391 | "Byte-ordering: little\n", 392 | "Detected cores: 8\n", 393 | "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n" 394 | ] 395 | }, 396 | { 397 | "data": { 398 | "text/plain": [ 399 | "8" 400 | ] 401 | }, 402 | "execution_count": 12, 403 | "metadata": {}, 404 | "output_type": "execute_result" 405 | }, 406 | { 407 | "name": "stdout", 408 | "output_type": "stream", 409 | "text": [ 410 | "In [12] used 7.031 MiB RAM in 0.043s, peaked 0.000 MiB above current, total RAM usage 188.711 MiB\n" 411 | ] 412 | } 413 | ], 414 | "source": [ 415 | "import bcolz\n", 416 | "bcolz.print_versions()\n", 417 | "bcolz.defaults.cparams['cname'] = 'lz4'\n", 418 | "bcolz.defaults.cparams['clevel'] = 9\n", 419 | "bcolz.defaults.cparams['shuffle'] = bcolz.BITSHUFFLE # try with NOSHUFFLE and BITSHUFFLE as well\n", 420 | "bcolz.set_nthreads(1)" 421 | ] 422 | }, 423 | { 424 | "cell_type": "code", 425 | "execution_count": 13, 426 | "metadata": { 427 | "collapsed": false 428 | }, 429 | "outputs": [ 430 | { 431 | "name": "stdout", 432 | "output_type": "stream", 433 | "text": [ 434 | "In [13] used 0.340 MiB RAM in 0.195s, peaked 78.199 MiB above current, total RAM usage 189.051 MiB\n" 435 | ] 436 | } 437 | ], 438 | "source": [ 439 | "zlens = bcolz.ctable.fromdataframe(lens)" 440 | ] 441 | }, 442 | { 443 | "cell_type": "code", 444 | "execution_count": 14, 445 | "metadata": { 446 | "collapsed": false 447 | }, 448 | "outputs": [ 449 | { 450 | "data": { 451 | "text/plain": [ 452 | "0.33984375" 453 | ] 454 | }, 455 | "execution_count": 14, 456 | "metadata": {}, 457 | "output_type": "execute_result" 458 | }, 459 | { 460 | "name": "stdout", 461 | "output_type": "stream", 462 | "text": [ 463 | "In [14] used 0.012 MiB RAM in 0.003s, peaked 0.000 MiB above current, total RAM usage 189.062 MiB\n" 464 | ] 465 | } 466 | ], 467 | "source": [ 468 | "size_bcolz = mw.measurements.memory_delta\n", 469 | "size_bcolz" 470 | ] 471 | }, 472 | { 473 | "cell_type": "markdown", 474 | "metadata": {}, 475 | "source": [ 476 | "Sometimes the memory reported as used is too much biased (don't know exactly why), so let's repeat the operation, but using a different container:" 477 | ] 478 | }, 479 | { 480 | "cell_type": "code", 481 | "execution_count": 15, 482 | "metadata": { 483 | "collapsed": false 484 | }, 485 | "outputs": [ 486 | { 487 | "name": "stdout", 488 | "output_type": "stream", 489 | "text": [ 490 | "In [15] used 0.062 MiB RAM in 0.190s, peaked 78.031 MiB above current, total RAM usage 189.125 MiB\n" 491 | ] 492 | } 493 | ], 494 | "source": [ 495 | "zlens2 = bcolz.ctable.fromdataframe(lens)" 496 | ] 497 | }, 498 | { 499 | "cell_type": "code", 500 | "execution_count": 16, 501 | "metadata": { 502 | "collapsed": false 503 | }, 504 | "outputs": [ 505 | { 506 | "data": { 507 | "text/plain": [ 508 | "7.253473281860352" 509 | ] 510 | }, 511 | "execution_count": 16, 512 | "metadata": {}, 513 | "output_type": "execute_result" 514 | }, 515 | { 516 | "name": "stdout", 517 | "output_type": "stream", 518 | "text": [ 519 | "In [16] used 0.000 MiB RAM in 0.002s, peaked 0.000 MiB above current, total RAM usage 189.125 MiB\n" 520 | ] 521 | } 522 | ], 523 | "source": [ 524 | "size_bcolz2 = zlens2.cbytes / 2**20.\n", 525 | "size_bcolz2" 526 | ] 527 | }, 528 | { 529 | "cell_type": "code", 530 | "execution_count": 17, 531 | "metadata": { 532 | "collapsed": false 533 | }, 534 | "outputs": [ 535 | { 536 | "data": { 537 | "text/plain": [ 538 | "ctable((1000209,), [('movie_id', '" 1207 | ] 1208 | }, 1209 | "execution_count": 41, 1210 | "metadata": {}, 1211 | "output_type": "execute_result" 1212 | }, 1213 | { 1214 | "data": { 1215 | "image/png": "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\ny277448/asmSJfLz87P/d9111yk3N1dpaWn2dtWrV9f111/vcO3AgQP1xx9/aPny5ZLOTTts0aKF\nYmJiyuR9xMbGOhyHhoYW6Ts0NFQZGRmSzk3zNMaoRYsWDu/nlVde0W+//SZJuv322xUeHq6wsDDd\ne++9mjlzpo4cOVKquI4fP64DBw7olltucTjfvn177dmzR6dPn7afu+mmm0rV9yOPPKJFixbp8OHD\nmjt3rh5++OEibVJTU1WtWjU1atTIfs7Ly0s333yzfvnlF0lSo0aN1LJlS33wwQeSzm1es3z5coc1\nWOc7fPiw9u7dq2HDhjncuzvuuEM2m007d+68ZOybNm265P0vdGFua9asqT/++EOSFBMTo06dOik6\nOlo9e/bU1KlTdeDAgUuOfzmuv/56BQUF2Y9DQ0MVGhqqqlWrOpwr/JkqyalTp+Tt7X3J8Ww2m8N7\nDQkJkbu7u/29PvbYY/b75O/v7/A+z7/OZrMpODjY4effZrMpJCTEHuumTZuUm5urmjVrOtz/jz76\nqEj+zu8nNDRUknTDDTc4nDPG2PtOTU1Vq1at5OHh4dBHQECA/WdO+nO/J7y9vYudVg3g8nlcugkA\n4Gpr0KCB3NzctHXrVt15551FXt+6dask2T/Mu7m5yRjj0ObCzTAKCgrUv39/vfDCC0XaVqtWzf7v\nKlWqFBmvatWq6tWrl2bOnKnbbrtNH3zwgV555ZUre3PFuLD4tNlsxZ4r3OygoKBANptNGzdutO/o\nd367wvexefNmrV+/XomJiXr77bf1/PPP69tvv1WzZs3KLPZCxd23i4mNjVV0dLTuvfdeeXp66o47\n7rjisQcMGKCxY8fqX//6l+bNm6fg4GDdfvvtxbYtvIdTp05VXFxckddr1ap1yfEu5/4X8vLyKvJ6\nYQxubm76+uuvtWnTJiUmJmrx4sUaMWKEPvnkE3Xp0uWScVxMaX+mShIcHFxkp8+SXPhepf/d73Hj\nxmn48OH28+fvPHklP/+BgYH24vliMZzfT2FuijtX2g2H/szviczMTAUHB5dqPACOKNoAwAUEBgaq\na9eumjZtmp5++ukiuy1OmDBB119/veLj4yXJ/ld4c96OjYWbVRRq0aKFtmzZUuw2/Jfj0UcfVYcO\nHTRjxgydPn1a99xzz0Xbe3l56ezZs1c01qXceOONks5ttnCxD/c2m03t2rVTu3btNHr0aEVFRWne\n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\nOnfurGHDhunQoUMKDg6+auOUFaZHAnA65vZbF7mzNvJnXeTO2lw9f4899pgOHDig++67T3Xq1NEb\nb7yh/fv3q1q1aiooKJAkde/eXePHj1fnzp1Vp04d9evXT0ePHtWjjz6qunXrKj4+XgcOHLD3uWPH\nDvXs2VP169fXzTffrM8++6zE8RMTE9W2bVv7cW5uroYMGaKIiAiFh4crPj5ehw8fliRlZ2frqaee\nUlRUlJo0aaLx48c7TOGcO3euWrVqpTp16qhNmzb6+eefJUmVKlVSbGysvv322zK9d1cLRRsAAAAA\nu7feeku1atXS/PnztW/fPj355JOSJJvN5tDus88+0zvvvKNffvlFu3btUufOnXX//fdr9+7datiw\noSZOnChJOnnypO6++2716dNHO3fu1KxZs/T8889rx44dxY6/bds2RURE2I/nz5+v48eP28f597//\nLW9vb0nS0KFD5eXlpeTkZK1evVqrVq3S+++/b49v0qRJmjFjhvbt26d58+YpKCjI3m/Dhg21devW\nsrtxVxFFGwCna9eunbNDwBUid9ZG/qyL3FmbVfJ3qa0vCp/E+fn5KT4+XmFhYbrlllvk5uamO++8\n0/5Ua/ny5apbt67uuece2Ww2NWnSRN26ddPSpUuL7TcrK0u+vr72Y09PT2VmZuq3336TzWZTTEyM\nfH19dejQISUmJmr8+PHy9vZWtWrVNGTIEC1ZskSS9OGHH+qpp55SbGysJCksLEy1atWy9+vn56es\nrKw/dY/KC2vaUGGkpB93dggAUCYud6MAAHCm89eCeXt7Fzk+ceKEJGn//v3atGmT6tWrJ+lcMXj2\n7Fn17du32H4DAgKUk5NjP77nnnuUnp6uBx98UNnZ2erTp49efPFF7d+/X/n5+YqMjLT3a4yxF2b/\n/e9/FR4eXmL8x48fV0BAwBW++/JF0YYK42ou2gWA8lReGwWsW7fOMn/xhyNyZ21WyN+FUyH/jOuv\nv15t27bV4sWLL6t9dHS0fvvtN/uxu7u7hg8fruHDh+vAgQPq3bu3IiIiFB8fL29vb/sTuOLG3b17\nd4nj7Nixo8TC0dUwPRIAAACAg5CQEO3Zs8fh3JV+U9hf/vIX/fbbb1q4cKHOnDmj/Px8/ec//ylx\nTdvtt9/usFnLunXrlJqaqoKCAlWpUkWenp5yd3dX9erV1aFDB/3jH//Q8ePHZYzRnj17tGHDBklS\n//79NW3aNKWkpEiSdu/ebd8cJTc3VykpKYqLi7ui91TeKNoAALhGufpf+lEycmdtVsjfM888o8mT\nJ6tevXqaPn26JMenb6V5Eufr66vFixfr008/VVRUlKKiojR27Fjl5+cX275z587auXOn/vjjD0nS\nH3/8ocGDByssLExt2rRRu3bt1KdPH0nSm2++qfz8fLVu3Vr16tXT4MGD7dfdeeedGjZsmB555BHV\nqVNH/fv317FjxyRJX3/9tdq1a3dVv1agLPHl2qgQkpKSNCK57B7jA4AzTeoSodiafs4OA8BV5spf\nru1s77//vn799VeNHz/+qvTfqVMnTZ06VY0bNy6xDV+uDQAAnM4K62pQPHJnbRfLXw3/SpYoqq62\nAQMGXNX+ExISrmr/ZY3pkQAAAADgwijaAAC4RvGkxrrInbWRP5QWRRsAAAAAuDCKNgAArlHnb6kN\nayF31kb+UFoUbQAAAIATuLu76+TJk84OAxcwxujIkSOqVMl1NoRh90gnee+993To0CH9/e9/d3Yo\nLunYsWN68sknNXr0aNWvX9/Z4QBAhcS6Gusid9ZWmL+QkBBlZGTYvzsMrsEYo4CAAPn6+jo7FDuK\nNic4ePCgEhMTr9r3TlQEgYGB6tixoz744AONHj3a2eEAAACUOZvNZpkvd4ZzMT3SCZYtW6awsDCF\nh4dftN2ZM2fKKaJLKygoUEFBQbGvXa044+PjtW3bNv32229XpX8AuNaxrsa6yJ21kT+UllOftC1c\nuFCLFy/W66+/rjlz5ig1NVV+fn7q0KGDevXqZW+3atUqvfXWW5o+fbquu+66Itd//PHH9nN9+/ZV\njx495Ovrq6+//lrZ2dmKjo7W448/LkmaNWuWUlJSVLlyZXXu3Fl33nlnkXFGjx6tL7/8Uj///LM8\nPT3Vpk0b9e/fX15eXjpz5oyGDBmiW2+9tciX/hVeP2XKlGK/PV06V+CsXbtWvXv3djifmpqqMWPG\n6Nlnn9V//vMf/fjjjzp79qxmz54tSdqzZ48+/vhjbd++Xfn5+QoPD1e/fv2KfIt7amqqPv30U+3c\nuVNnz55VaGiounTpog4dOkiSNmzYoMTERO3bt095eXmqUaOGunTpovbt2zv0U3gfvb29lZiYqCNH\njmjChAk6efJkiXEePHhQn3zyibZv365jx44pKChIsbGxuvfee1WlShVJ0pdffqn58+fr7bfflp+f\nn8OYTzzxhBo0aKCnn35aklSrVi3VqVNHSUlJTJEEAADANcupRZvNZpMkTZ48WXFxcerWrZs2bdqk\nRYsW6brrrlNcXNxlXX+htWvXqnbt2nrooYeUlZWl2bNna9q0aTp16pSaNWum22+/XRs3btS8efNU\nt25dNW3a1OH6adOmqXXr1vrLX/6inTt36pNPPlFubq4ef/xxeXh4KC4uTitXrtR9990nD4//3cLE\nxERFR0eXWLBJ0o4dO3Ty5MkixVah2bNnq2nTpnryySeVn58vSdq1a5dGjRqlevXqaciQIapUqZIS\nEhI0btw4vfzyy/Yndj/++KP+/e9/q3HjxnrkkUfk7++v/fv369ChQ/b+Dx48qJtuukl33nmn3N3d\nlZqaqhkzZig/P1/x8fEOsaxatUrVq1fXgAEDVKlSJVWtWtW+WLa4OI8ePaqqVatq4MCB8vX1VUZG\nhpYsWaJXX31V48aNkyTFxcVpwYIFWrlypbp3724f66efftKhQ4c0dOhQhxgiIyO1efPmEu8nAODK\nsS7KusidtZE/lJZLrGn761//an/S06RJE23dulXr16+/ZNFWEk9PTz3//PNyczs3+3Pfvn366quv\ndM8996hHjx6SpKioKP3www/auHFjkaKtWbNmuv/++yVJMTExkqRFixapZ8+eCg0NVadOnfTll19q\n48aNuuWWWyRJe/fuVVpamp555pmLxpaWliabzaa6desW+3pERIQeffRRh3MffvihQkJCNGrUKPt7\nio2N1bBhw7R48WI999xzkqQ5c+YoPDxco0aNsl/bpEkTh7569uxp/7cxRlFRUTp69KgSEhKKFG2S\nNHLkSIfC9GJxRkZGKjIy0n7cqFEjVa9eXaNGjdKePXsUFhYmX19ftWnTRklJSQ5FW2Jioq6//nqH\n6yUpLCxMy5cv17FjxxQYGFjsPQMAAAAqMpco2po1a+ZwXLt2be3Zs+eK+4uJibEXN5LsT75iY2Pt\n59zc3BQaGqojR44Uub5169YOx23bttXHH3+snTt3KjQ0VCEhIYqNjVViYqK9aFuxYoX8/f110003\nXTS2o0ePqnLlynJ3dy/29ZYtWzoc5+Xladu2bfZis3BdmTFGN9xwg9avXy9JSk9P1+HDh+3tSnLw\n4EEtWLDAPoXRGCPpXKF7oaZNmxZbsBUXp3Ru6ufnn3+utWvX6tChQ/YncIXxhYWFSZI6deqk1atX\n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\n9NJLL2nJkiWaNGmSTp8+rb179+qFF17Q0KFDvetRf71nt+yoPXOsyK+lpUWnTp3SvHnz9PLLL2v6\n9Om6deuWiouLFR0drbFjx3rX61ftGYCF9u/fb+Tm5hpXr171Hrt69aqRm5tr7Nu375Fz6+rqjJyc\nHOOHH37wHuvo6DDef/9949NPP/Ueu3XrlrFy5UqjuLjYZ/4nn3xifPDBBz5z3377bWPz5s0+47Zs\n2WKsWbPG6Ojo6NM1BjM75WcYhpGfn298/PHH/bmkAcOK7Lrq6OgwcnJyuuXo+TNqzxw75WcY1J5Z\nVuTX3Nzcba7b7Tby8vKMTZs2+cyl/nrPTtkZBrVnltXvnV1t2LDhP/13J8+0wVLHjx/X2LFjlZCQ\n4D2WkJCgcePG6dixY4+ce+zYMTkcDs2cOdN7LCQkRLNmzdKpU6fU3t4uSTp58qTa29s1Z84cn/lz\n585VfX29rl27Jkk6d+6cbt++3e2j6/T0dDU3N+vs2bP9utZgZKf8YI4V2fUWtWeenfKDeVbkFxMT\n021uVFSUEhMTdfPmTe8x6s8cO2UH8/z53hkTE6OQkPutVn9rj6YNlmpoaFBSUlK34yNHjtTFixcf\nOffixYtKSEhQWFiYz/GkpCS1t7frypUr3nFPPfWUz210nnN4/tyzF8/8B9frOg732Sk/j7q6OuXl\n5WnFihVat26dDh48aPq6BgIrsjOzF8/8B9fznA++7JSfB7XXe/7Kz+Vyqb6+3vv+6dmLZ/6D63nO\nh/vslJ0Htdd7VufX2dkpl8slp9Op06dPKzMz02cvnvkPruc536Pwg0hgKZfLpejo6G7HY2Ji5HK5\nHjnX7Xb3ONez9uPO8eC4rsd7Gof77JSfJE2YMEFz587ViBEj5Ha7VVVVpc8//1xNTU16/fXXe39h\nA4AV2ZnZS9f5/V1vILBTfhK1Z5a/8isqKpIknx9QQf2ZY6fsJGrPLCvzKy8v144dOyRJDodDeXl5\nPp+q9bf2aNoABKycnByf30+bNk0FBQUqLS3V4sWLFR4e7qedAcGN2rO/0tJSHTlyRGvXrtXTTz/t\n7+3AhEdlR+3Z1+zZs5Wamqrm5mYdO3ZMRUVFCgkJ0fz58/+T9bk9EpaKjo6W2+3udtzlcj30nu7e\nzpXu/58KM+O6Hu9pHO6zU349mT17tlpbW1VfX//IcQONFdmZ2UvX+f1dbyCwU349ofZ6ZnV+3333\nnfbs2aMVK1boxRdf7LZe1/m9WW8gs1N2PaH2emZlfrGxsRo9erTS0tK0Zs0apaen66uvvlJnZ6d3\nva7zH7feg2jaYKmkpKSH3rN76dKlh9633dXIkSPV2Nio1tZWn+MNDQ1yOBzeZ6CSkpLU1tamq1ev\n+ozznNdznp7uIX5wHO6zU34wx4rszOxFovbMsFN+MM/K/KqqqlRYWKglS5Zo6dKlD92LRP31lp2y\ng3n+fO8cPXq0Wlpa1NTU5N2L1Pfao2mDpaZNm6Zz586psbHRe6yxsVFnz57VtGnTHju3vb1dNTU1\n3mOdnZ06evSo0tLS5HD8e7fv5MmTFRoaqurqap/51dXVSk5O1vDhwyVJqampio2N7TauqqpKMTEx\nGjduXL+uNRjZKb+eVFdXKywsTMnJyWYvL6hZkV1vUXvm2Sm/nlB7PbMqv59++klbt27V/PnztWrV\nqoeuR/2ZY6fsekLt9cyf751nzpxRRESE4uLiJPW/9vhybVgqOTlZP/74o2prazVs2DBdvnxZ27Zt\nU3h4uN555x1vAVy/fl2rV6/WoEGDvF/sOmTIEF26dEkHDhxQbGys3G63vv76a/3555967733vF9y\nGB4ernv37umbb75RWFiY2tvbVVZWptraWq1du1YjRoyQ9O+PbY2MjFRpaana29sVEhKigwcPqry8\nXG+++abPlyHiX3bK7+zZsyoqKlJbW5tcLpf++usv7dq1S8ePH1dubq4mTpzonxfJpqzITpIuXLig\nP/74Qw0NDaqtrfX+x8rzU7hCQ0OpvT6wU37UnnlW5HfmzBlt3LhRo0aN0tKlS3Xz5k3vr+bmZu84\n6s8cO2VH7ZlnRX5Op1Pl5eW6d++eXC6X6urqVFxcrJqaGi1fvlwTJkyQ1P/aG2QYhvEEXyugmxs3\nbmjnzp06ffq0DMPQpEmTlJeXp/j4eO+Ya9eu6d1331V2draysrK8x9va2rR7924dOXJEbrdbo0aN\n0qpVq7wF4WEYhkpLS1VRUaGmpiYlJiYqOztb06dP77Yfp9Opffv26dq1a4qPj1dmZqYWLFjw5F6A\nAGeX/K5cuaIdO3aovr5et2/flsPhUHJyshYuXKhZs2Y9+RciAFmR3ZYtW3To0KGHnn/z5s0+56L2\nzLFLftRe3zzp/IqLi1VSUvLQcw8fPlybNm3yOUb99Z5dsqP2+uZJ53fu3Dnt3btXdXV1crlcGjx4\nsJ599lllZmZq8uTJ3fbT19qjaQMAAAAAG+OZNgAAAACwMZo2AAAAALAxmjYAAAAAsDGaNgAAAACw\nMZo2AAAAALAxmjYAAAAAsDGaNgAAAACwMZo2AAAAALAxmjYAAAAAsLH/B1+KiP4jTnbPAAAAAElF\nTkSuQmCC\n", 1216 | "text/plain": [ 1217 | "" 1218 | ] 1219 | }, 1220 | "metadata": {}, 1221 | "output_type": "display_data" 1222 | }, 1223 | { 1224 | "name": "stdout", 1225 | "output_type": "stream", 1226 | "text": [ 1227 | "In [41] used 0.832 MiB RAM in 0.204s, peaked 0.000 MiB above current, total RAM usage 475.625 MiB\n" 1228 | ] 1229 | } 1230 | ], 1231 | "source": [ 1232 | "# Query times\n", 1233 | "labels = [\"pandas\", \"bcolz\", \"uncompressed bcolz\", \"numpy (recarray)\"]\n", 1234 | "df = pd.DataFrame({'time (sec)': [qtime_pandas, qtime_bcolz, qtime_ubcolz, qtime_numpy]}, index=labels)\n", 1235 | "pd.options.display.mpl_style = 'default'\n", 1236 | "df.plot(kind='barh', figsize=(12,5), fontsize=16, title=\"Query times for MovieLens 1m (in-memory)\")" 1237 | ] 1238 | }, 1239 | { 1240 | "cell_type": "markdown", 1241 | "metadata": {}, 1242 | "source": [ 1243 | "## Size comparison" 1244 | ] 1245 | }, 1246 | { 1247 | "cell_type": "code", 1248 | "execution_count": 42, 1249 | "metadata": { 1250 | "collapsed": false, 1251 | "scrolled": true 1252 | }, 1253 | "outputs": [ 1254 | { 1255 | "data": { 1256 | "text/plain": [ 1257 | "" 1258 | ] 1259 | }, 1260 | "execution_count": 42, 1261 | "metadata": {}, 1262 | "output_type": "execute_result" 1263 | }, 1264 | { 1265 | "data": { 1266 | "image/png": "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\nBaDgoDADAGTx888/GzabzVi6dOlNj0tNTTWKFy9ufPbZZy77u3btarRr1865bbPZjMGDB7scU69e\nPeMf//iHczsoKMgYOXJkrnObOHGiUbt2bed2ToXZrl27nPt++uknw263GwcOHDAMwzCGDx9uNG/e\n3OW6DofDqFmzpjF58mSX62QWVDnZuXOnYbfbjaNHj2b7+pEjRwybzZbtdXbu3Gl4e3u75Fe9enVj\n+vTpLsdt3LjRsNlsxvnz541z584Zdrvd2LBhw03nldsc+vTpYzRt2tTluGXLlhl2u904duyYYRjX\nCrMyZcoYqampuY6RWZgtXLjQCAwMNAzDMH744QfDz8/PSE9PdynMzPzcOBwO45577jEmTJjgfH38\n+PFG1apVnds3fvbPPPOM8eSTT7pc8/Lly4aXl5exbNmybM+5UW75G8a1wqxhw4YuxwwYMMC47777\nnNu+vr7GnDlzchznZnIrzL777jvnvkWLFmX53+rSpUsNu91uXLx48bbGB2ANljICALIwDMPUcXFx\ncUpPT1erVq1c9rdu3Vq//vqry76wsDCX7cqVK+vUqVO5jvHFF1+oWbNm8vf3V6lSpfT+++/r6NGj\nNz3HZrO59L9VrlxZhmE4x9u+fbu2b9+uUqVKOf/4+Pjo6NGjOnjwoMu1mjRpctOxQkND1aFDB4WE\nhKhbt276+OOP9ccff+T6vk6cOKEuXbropZde0ssvvyxJSkhI0NGjR/XGG2+4zO3hhx+WzWZTXFyc\nSpcureeff14dOnRQp06dNG7cOB04cCDX8W7066+/6v7773fZ17p1axmGodjYWOe+evXqqUSJEqav\n+/jjjys1NVVr167VF198oX79+mXpSzPzc2Oz2dS7d2/NnTvX+fpXX32l3r175zj2tm3btHTpUpfs\nypcvrytXrmT5XLNjJv9Muf08v/XWW3r++efVtm1bjRw5Ujt37sx1fDNu/Nn29/eXzWZTgwYNXPZJ\n0unTp/NkTAD5g4d/AACyqFWrlux2u2JjY/X444/f9FizRdyNjzW32Wy5Ptlx0aJFGjRokP7973/r\n/vvvl4+PjxYuXKihQ4fe9Dy73e7ygIfMv2eO53A41L59e02dOjXL/H19fZ1/L1asWK6PY8/s+dm+\nfbsiIyO1ZMkSvffee1q8eLE6deqU7TmXLl1Sly5d1LhxY3300UfO/Znz+/jjj9WmTZss51WpUkWS\n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\ndRYuXKglS5Zo4sSJmjVrlvbv369SpUqpZ8+eatOmjTZu3KilS5fq7Nmzqlmzpvr376+KFSs6zx84\ncKDq1q1k4QQNAAAgAElEQVSr4OBgLVu2TGfPnlWVKlXUt29fhYSEuGSwd+9e/f3vf9fcuXN1+PBh\ntW/fXs8884wkKTIyUqtXr1Z8fLw8PT117733qnfv3vL29nZeY+XKlVq7dq3OnDkjd3d3+fv7q1u3\nbrr33nslSbt27dKSJUt0/PhxORwOlS1bVq1atVL37t2d1zhy5IgWLFig/fv3Kz09XYGBgXr66adV\nt25dl89m5cqV+v7773X+/HkFBASoX79+uX3MLi5evKhPP/1U27Ztk8PhUOPGjfXcc8+5vJ+8+gxv\nNHLkSMXGxmb72tSpU1W+fPlbei8AAADIHT1m1su1MLPZbNkuN8tpCdqcOXMUHh6u119/XfHx8frq\nq69UrFgxvfLKK85jJk6cqO3bt+uRRx5RgwYNlJ6ertjYWJ0/f16VK1fWuXPnNGzYMHl5eTmLutWr\nV+vDDz/Ue++95/zFPnMOEydOVLt27dSlSxetXr1a06ZN04kTJxQbG6unn35aGRkZmjVrlj7++GON\nGTPGZb6xsbE6fPiwnnrqKbm5uWnZsmUaO3asxo8fr0qVKjnHSU1N1eTJk/Xoo4/qySeflIeHhyTp\n66+/1ooVK/TII4+oT58+SkxM1Pz583X8+HGNHj1aNptNmzZt0ty5cxUREaG6desqLS1NR48eVUpK\niiTp9OnTGj9+vJo3b64ePXrIzc1NJ06c0OnTp53zPHTokIYPH64aNWqof//+Kl68uNasWaNRo0Zp\n9OjRCgwMlCT9+OOPmjNnjtq2bavmzZvr5MmTmjx5si5fvpzbR+00e/ZshYaGavDgwTpx4oTmzZun\n8+fP65///Geef4Y3euGFF3Tp0iXntsPh0NSpU3X58mWXwhAAAAAoTHItzG5VvXr19Oyzz0qSQkND\nFR8frx9//NFZmO3du1c///yznn32WXXs2NF5XpMmTZx/X758uVJTUzV27Fj5+flJunbn7Y033tD8\n+fOz/FLfpUsXtWrVSpJUo0YN7dixQ5GRkZo6dao8PT0lSefOndPs2bOVkJDgctclOTlZY8aMUdmy\nZSVJ9evX1yuvvKIlS5Zo0KBBzuMuX76s1157TY0bN3buO3PmjJYvX66ePXuqW7duzv2VK1fWsGHD\ntGPHDjVp0kQHDx5UtWrVXI65/j0cOnRIGRkZeuGFF5zzvf6OnSR99dVX8vPz0/Dhw2W3X1uBGhYW\npjfeeENLlizRW2+9JcMwtGjRIjVs2FD9+/d3HlOqVClNnjw5x8/sRgEBARowYIDzfG9vb33yySfa\nu3ev6tevf0c+w0z33HOPy/aMGTOUmJioESNGOLMBAABA3oqOjuaumcXyvMcsPDzcZTsgIEDp6elK\nSkqSJO3Zs0c2m03t2rXL8Rr79+9X7dq1nb/QS5LdbleLFi105MiRLHd/rv8lv2TJkvLx8VGtWrVc\nfpHP/IX/7NmzLufWqlXLWZRJkqenp8LDw3Xw4EGX49zc3LK8tz179sgwDLVs2VIOh8P5p2bNmvL0\n9HQuyatZs6aOHDmimTNn6pdfflFaWprLdapXry43NzdNnDhR//vf/5ScnOzyelpamvbt26emTZtK\nkstYDRo00L59+5zvLTExUc2bN3c5v1mzZs5izozszrfZbDpw4IAkaffu3Xn+GWZn1apVWrt2rQYN\nGqSaNWuanj8AAABwt8nzO2Y3Ljdzc7s2RHp6uiTpwoUL8vb2lru7e47XSElJcS7Nu17p0qVlGIZS\nUlJciq6SJUtmGTO3eWTy9fXNMo6vr68SExNd9vn4+GRZvplZbL766qs5vg9Jat26tdLT07Vu3Tqt\nXbtWdrtdjRo1Ur9+/VShQgX5+/tryJAhWrZsmaZOnaq0tDQFBQXp6aefVnBwsFJSUuRwOLRkyRIt\nWbIkyziZ8zp//ny278lut6tUqVLZzjE7N57v5uamkiVLOjNJSUnJ88/wRrt379acOXP0xBNPOAtS\nAAAA/J/MJylm3un6K9stW7bM0+sV5u3rn52Rl3ItzNzd3bN9AuGFCxdu6Zf9TKVKlVJKSorS09Nz\n/MXe29vbWWRc79y5c7LZbHnaa5RZXN247/q7aDnJfP+ZvVQ5vS5J7du3V/v27ZWamqrdu3fryy+/\n1KRJk5w9b8HBwQoODlZGRoZ+++03LViwQOPGjdPUqVNVsmRJ2Ww2dezYUa1bt5ZhGNnOp3Tp0tm+\nJ4fDoQsXLuT6fjLdeH5GRoYuXrzozOROf4Z//PGHJk2apNatW+vxxx83PW8AAICi5Malh2znz3ZM\nTIzuhFzXt1WoUEFJSUkuv9ifPHlS8fHxtzVgWFiYDMNQVFRUjscEBwfr4MGDSkhIcO5zOBzaunWr\n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\nz549qlmzpktRdr0rV67o8OHDuu+++5xFmST5+fmpbt262rdvn6k5TJkyRadPn9Z7770nb2/v238z\nAAAAQAHHHTNkq3Tp0i7bvr6+kq4tLUxJScmxKJOkixcvSlK2yxV9fX2VkJCQ6/jz58/Xjh07NHTo\nUPn7+9/K1PNF5nd9ZP7r0t2ynbmvoMynIG//8ssvGjBgQIGZT0HdvvFny+r5FOTtGzOzej4FdXva\ntGlq0KBBgZlPQd3O3FdQ5lOQt/nvOf89z+ttLy8v3Qk2wzCMO3Jl3JUWLVqkxYsX65NPPpGfn59z\n/969ezVq1Cj961//0ty5c2Wz2TRq1Khsr3HlyhX17dtX3bt3d3lgiCSNHDlSly9f1tixY53bkjR8\n+HDnMdHR0frkk080cOBA3X///abmHRUVpfdi8ufR++M7BSmscql8GSuvRUfT2GsWWZlDTuaRlTnk\nZA45mUdW5pCTeTExMWrXrl2eX5eljMjW1q1bXbajo6Pl6empgIAAhYaGKi4uTseOHcv23OLFi6tG\njRraunWrrq/7z5w5o99++00hISE5jnvgwAFNmzZNXbt2NV2UwTz+g2seWZlDTuaRlTnkZA45mUdW\n5pCT9dysngAKpqioKDkcDtWsWVO7du3SunXr1LNnT5UoUUKdO3fW5s2bNWrUKHXt2tX5VMbt27fr\npZdekqenp3r16qVx48bpww8/VIcOHXTp0iUtWrRIJUuWVOfOnbMd89KlSxo/fryqVKmi8PBwHTx4\n0Pmau7u7qlevnk/vHgAAAMhfFGbIwmaz6Z133tGMGTP0zTffyMvLS927d1f37t0lXVtXO2rUKM2f\nP1/Lli1TSkqKfH19Vb9+fed3mDVs2FDvvfeeFi9erEmTJrl8j9mN/Ws227UliCkpKUpOTlZycrKG\nDRvmcgzfY5Y3WKZgHlmZQ07mkZU55GQOOZlHVuaQk/UozOAiIiJCERERklz7vm7k4+Ojl1566abX\nCgsLU1hY2E2PuX6MChUqaMGCBbcwWwAAAKBw4OEfKBR4+AcAAADyAw//AAAAAIBCisIMKEKu/44S\n3BxZmUNO5pGVOeRkDjmZR1bmkJP1KMwAAAAAwGL0mKFQoMcMAAAA+YEeMwAAAAAopCjMgCKE9ePm\nkZU55GQeWZlDTuaQk3lkZQ45WY/CDAAAAAAsRo8ZCgV6zAAAAJAf6DEDAAAAgEKKwgwoQlg/bh5Z\nmUNO5pGVOeRkDjmZR1bmkJP1KMwAAAAAwGL0mKFQiIqKUjH/Wvkylp+3hyr5FM+XsQAAAFCw3Kke\nM7c8vyJgER7IAQAAgLsVSxmBIoT14+aRlTnkZB5ZmUNO5pCTeWRlDjlZj8IMAAAAACxGjxkKhaio\nKIWHh1s9DQAAABRyfI8ZAAAAABRSFGZAEcL6cfPIyhxyMo+szCEnc8jJPLIyh5ysR2EGAAAAABaj\nxwyFAj1mAAAAyA/0mAEAAABAIUVhBhQhrB83j6zMISfzyMoccjKHnMwjK3PIyXoUZgAAAABgMXrM\nUCjQYwYAAID8QI8ZAAAAABRSFGZAEcL6cfPIyhxyMo+szCEnc8jJPLIyh5ysR2EGAAAAABajxwyF\nAj1mAAAAyA/0mAEAAABAIUVhBhQhrB83j6zMISfzyMoccjKHnMwjK3PIyXoUZgAAAABgMXrMUCjQ\nYwYAAID8QI8ZAAAAABRSFGZAEcL6cfPIyhxyMo+szCEnc8jJPLIyh5ysR2EGAAAAABajxwyFAj1m\nAAAAyA/0mAEAAABAIUVhBhQhrB83j6zMISfzyMoccjKHnMwjK3PIyXoUZgAAAABgMXrMUCjQYwYA\nAID8QI8ZAAAAABRSFGZAEcL6cfPIyhxyMo+szCEnc8jJPLIyh5ysR2EGAAAAABajxwyFAj1mAAAA\nyA/0mAEAAABAIeVm9QSAvLI7/oLVUyjwkpKS5Ovra/U07gpkZQ45mUdW5pCTOeRkHlmZk5SUpFr3\nVFAln+JWT6XIojBDofH2yjirp3CXOGP1BO4iZGUOOZlHVuaQkznkZB5ZmTG+ky+FmYVYyggAAAAA\nFqMwAwAAAACLUZgBAAAAgMUozAAAAADAYhRmAAAAAGAxCjMAAAAAsBiFGQAAAABYjMIMAAAAACxG\nYQYAAAAAFqMwwx2xfv169erVSwkJCVZPBQAAACjwKMwAAAAAwGIUZgAAAABgMTerJ4C8s3DhQi1Z\nskTjx4/XrFmzFBcXJy8vL7Vr1049e/aUJKWnp+u///2v9uzZozNnzsjT01M1a9ZUnz59VLlyZee1\n1q9fr2nTpmn06NH64YcftGPHDnl6eqpZs2bq06eP3Nz+70fn9OnTmjFjhmJjY+Xp6akWLVqoSpUq\nWea3ZcsWRUZG6tixY0pLS1OlSpXUqVMntW7d2uW4lStXau3atTpz5ozc3d3l7++vbt266d57771D\nyQEAAADWojArRGw2myRpwoQJatu2rbp27ardu3dryZIlstvt6tGjh9LT03Xp0iV169ZNZcqU0cWL\nF7VmzRoNGTJEkyZNkq+vr8s1p0yZohYtWujtt9/WgQMHtHDhQnl7eysiIkKSlJGRoVGjRik9PV0v\nvPCCfHx8tHbtWv30009Z5nfy5En97W9/02OPPaZixYopNjZW06dPV3p6utq3by9J2rRpk+bOnauI\niAjVrVtXaWlpOnr0qFJSUu5wegAAAIB1KMwKofbt26tLly6SpNDQUKWmpmr58uXq1KmTvLy81L9/\nf+exDodDYWFhevHFF7V582Z16tTJ5VqtWrVSjx49JEn169fXgQMHtHnzZmdhtn79ep0+fVpjxoxR\nUFCQJKlhw4Z66623lJiY6HKtbt26Of9uGIaCg4N17tw5rVmzxlmYHTx4UNWqVXM5tmHDhnkVDQAA\nAFAgUZgVQs2aNXPZbtGihX788UcdP35cderU0ZYtW/T9998rPj5eqampzuPi4+OzXKtRo0Yu2wEB\nAdq7d69z++DBgypfvryzKJOu3blr3ry5Fi1a5HLuyZMnNX/+fO3fv1/nz5+XYRiSJHd3d+cxNWvW\n1Jo1azRz5kzde++9qlOnjjw8PG4jBQAAAODuQWFWCJUuXdplO3N5YmJionbs2KHJkyerTZs2ioiI\nUKlSpWSz2TR27FilpaVluZa3t7fLtru7u9LT053b58+fz7L88foxM12+fFmjRo2Sp6enevfuLT8/\nP7m5uWnNmjVat26d87jWrVsrPT1d69at09q1a2W329WoUSP169dPFSpUuPUwAAAAYEpSUpKiD+1W\ny5YtJUnR0dGSxPYN215eXtkH+BdRmBVC58+fl5+fn3M7KSlJklS2bFmtXr1a/v7+GjBggPP1q1ev\n3nYPV+nSpfXHH39kO4frHThwQAkJCRo1apRq167t3J+RkZHl3Pbt26t9+/ZKTU3V7t279eWXX2rS\npEkaM2bMbc0RAAAAufP19VVYvZbO7cyChG3X7ZiYGN0JPC6/ENq6davLdnR0tDw9PRUQEKC0tDQV\nK1bM5fUNGzbI4XDc1li1a9dWQkKC4uLinPsMw8gyh8y7cXb7//3IpaSkaMeOHTle28vLS82bN1fz\n5s11/Pjx25ofAAAAcDfgjlkhFBUVJYfDoZo1a2rXrl1at26devbsqRIlSqhhw4batm2b5syZo8aN\n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\nAElFTkSuQmCC\n", 1267 | "text/plain": [ 1268 | "" 1269 | ] 1270 | }, 1271 | "metadata": {}, 1272 | "output_type": "display_data" 1273 | }, 1274 | { 1275 | "name": "stdout", 1276 | "output_type": "stream", 1277 | "text": [ 1278 | "In [42] used 0.914 MiB RAM in 0.213s, peaked 0.000 MiB above current, total RAM usage 476.539 MiB\n" 1279 | ] 1280 | } 1281 | ], 1282 | "source": [ 1283 | "# Container sizes\n", 1284 | "df = pd.DataFrame({'size (MB)': [size_pandas2, size_bcolz2, size_ubcolz2, size_numpy3]}, index=labels)\n", 1285 | "pd.options.display.mpl_style = 'default'\n", 1286 | "df.plot(kind='barh', figsize=(12,5), fontsize=16, title=\"Container sizes for MovieLens 1m\")" 1287 | ] 1288 | }, 1289 | { 1290 | "cell_type": "markdown", 1291 | "metadata": {}, 1292 | "source": [ 1293 | "## Rules of thumb for querying in-memory tabular datasets\n", 1294 | "\n", 1295 | "* Choose pure NumPy recarrays if you need the fastest speed\n", 1296 | "* Choose bcolz ctables if you need to store lots of data in limited memory and not want to loose too much speed\n", 1297 | "* Choose pandas if what you need is rich functionality on top of your containers (at the penalty of some speed)" 1298 | ] 1299 | } 1300 | ], 1301 | "metadata": { 1302 | "kernelspec": { 1303 | "display_name": "Python 2", 1304 | "language": "python", 1305 | "name": "python2" 1306 | }, 1307 | "language_info": { 1308 | "codemirror_mode": { 1309 | "name": "ipython", 1310 | "version": 2 1311 | }, 1312 | "file_extension": ".py", 1313 | "mimetype": "text/x-python", 1314 | "name": "python", 1315 | "nbconvert_exporter": "python", 1316 | "pygments_lexer": "ipython2", 1317 | "version": "2.7.11" 1318 | } 1319 | }, 1320 | "nbformat": 4, 1321 | "nbformat_minor": 0 1322 | } 1323 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Notebooks and other materials for the Data Containers tutorial 2 | ============================================================== 3 | 4 | You can get the latest release of the materials here: 5 | 6 | https://github.com/FrancescAlted/DataContainers/releases 7 | 8 | Also, make sure that you have the next Python packages installed: 9 | 10 | * numpy 11 | * numexpr 12 | * pandas 13 | * bcolz 14 | * tables (pytables) 15 | * matplotlib 16 | * psutil 17 | * memory_profiler 18 | * ipython_memwatcher 19 | 20 | I recommend to use Anaconda to install most of the packages above, and for 21 | the software that is not in anaconda.org repos, just use pip, e.g.:: 22 | 23 | $ pip install ipython_memwatcher 24 | 25 | and start by the different tutorials following the numerical order. 26 | 27 | ** Enjoy data! ** 28 | -------------------------------------------------------------------------------- /movielens-1m/README: -------------------------------------------------------------------------------- 1 | SUMMARY 2 | ================================================================================ 3 | 4 | These files contain 1,000,209 anonymous ratings of approximately 3,900 movies 5 | made by 6,040 MovieLens users who joined MovieLens in 2000. 6 | 7 | USAGE LICENSE 8 | ================================================================================ 9 | 10 | Neither the University of Minnesota nor any of the researchers 11 | involved can guarantee the correctness of the data, its suitability 12 | for any particular purpose, or the validity of results based on the 13 | use of the data set. The data set may be used for any research 14 | purposes under the following conditions: 15 | 16 | * The user may not state or imply any endorsement from the 17 | University of Minnesota or the GroupLens Research Group. 18 | 19 | * The user must acknowledge the use of the data set in 20 | publications resulting from the use of the data set, and must 21 | send us an electronic or paper copy of those publications. 22 | 23 | * The user may not redistribute the data without separate 24 | permission. 25 | 26 | * The user may not use this information for any commercial or 27 | revenue-bearing purposes without first obtaining permission 28 | from a faculty member of the GroupLens Research Project at the 29 | University of Minnesota. 30 | 31 | If you have any further questions or comments, please contact Sean McNee 32 | . 33 | 34 | ACKNOWLEDGEMENTS 35 | ================================================================================ 36 | 37 | Thanks to Shyong Lam and Jon Herlocker for cleaning up and generating the data 38 | set. 39 | 40 | FURTHER INFORMATION ABOUT THE GROUPLENS RESEARCH PROJECT 41 | ================================================================================ 42 | 43 | The GroupLens Research Project is a research group in the Department of 44 | Computer Science and Engineering at the University of Minnesota. Members of 45 | the GroupLens Research Project are involved in many research projects related 46 | to the fields of information filtering, collaborative filtering, and 47 | recommender systems. The project is lead by professors John Riedl and Joseph 48 | Konstan. The project began to explore automated collaborative filtering in 49 | 1992, but is most well known for its world wide trial of an automated 50 | collaborative filtering system for Usenet news in 1996. Since then the project 51 | has expanded its scope to research overall information filtering solutions, 52 | integrating in content-based methods as well as improving current collaborative 53 | filtering technology. 54 | 55 | Further information on the GroupLens Research project, including research 56 | publications, can be found at the following web site: 57 | 58 | http://www.grouplens.org/ 59 | 60 | GroupLens Research currently operates a movie recommender based on 61 | collaborative filtering: 62 | 63 | http://www.movielens.org/ 64 | 65 | RATINGS FILE DESCRIPTION 66 | ================================================================================ 67 | 68 | All ratings are contained in the file "ratings.dat" and are in the 69 | following format: 70 | 71 | UserID::MovieID::Rating::Timestamp 72 | 73 | - UserIDs range between 1 and 6040 74 | - MovieIDs range between 1 and 3952 75 | - Ratings are made on a 5-star scale (whole-star ratings only) 76 | - Timestamp is represented in seconds since the epoch as returned by time(2) 77 | - Each user has at least 20 ratings 78 | 79 | USERS FILE DESCRIPTION 80 | ================================================================================ 81 | 82 | User information is in the file "users.dat" and is in the following 83 | format: 84 | 85 | UserID::Gender::Age::Occupation::Zip-code 86 | 87 | All demographic information is provided voluntarily by the users and is 88 | not checked for accuracy. Only users who have provided some demographic 89 | information are included in this data set. 90 | 91 | - Gender is denoted by a "M" for male and "F" for female 92 | - Age is chosen from the following ranges: 93 | 94 | * 1: "Under 18" 95 | * 18: "18-24" 96 | * 25: "25-34" 97 | * 35: "35-44" 98 | * 45: "45-49" 99 | * 50: "50-55" 100 | * 56: "56+" 101 | 102 | - Occupation is chosen from the following choices: 103 | 104 | * 0: "other" or not specified 105 | * 1: "academic/educator" 106 | * 2: "artist" 107 | * 3: "clerical/admin" 108 | * 4: "college/grad student" 109 | * 5: "customer service" 110 | * 6: "doctor/health care" 111 | * 7: "executive/managerial" 112 | * 8: "farmer" 113 | * 9: "homemaker" 114 | * 10: "K-12 student" 115 | * 11: "lawyer" 116 | * 12: "programmer" 117 | * 13: "retired" 118 | * 14: "sales/marketing" 119 | * 15: "scientist" 120 | * 16: "self-employed" 121 | * 17: "technician/engineer" 122 | * 18: "tradesman/craftsman" 123 | * 19: "unemployed" 124 | * 20: "writer" 125 | 126 | MOVIES FILE DESCRIPTION 127 | ================================================================================ 128 | 129 | Movie information is in the file "movies.dat" and is in the following 130 | format: 131 | 132 | MovieID::Title::Genres 133 | 134 | - Titles are identical to titles provided by the IMDB (including 135 | year of release) 136 | - Genres are pipe-separated and are selected from the following genres: 137 | 138 | * Action 139 | * Adventure 140 | * Animation 141 | * Children's 142 | * Comedy 143 | * Crime 144 | * Documentary 145 | * Drama 146 | * Fantasy 147 | * Film-Noir 148 | * Horror 149 | * Musical 150 | * Mystery 151 | * Romance 152 | * Sci-Fi 153 | * Thriller 154 | * War 155 | * Western 156 | 157 | - Some MovieIDs do not correspond to a movie due to accidental duplicate 158 | entries and/or test entries 159 | - Movies are mostly entered by hand, so errors and inconsistencies may exist 160 | -------------------------------------------------------------------------------- /movielens-1m/ratings.dat.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrancescAlted/DataContainersTutorials/419651d999efa166e14a185a3cbaad75212f9e4b/movielens-1m/ratings.dat.gz --------------------------------------------------------------------------------