From 7c0f4e9ec747ec51388e7a039d2b6904d1b31b29 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Tue, 27 Mar 2018 18:43:37 +0200 Subject: [PATCH 1/3] Add performance pandas vs numpy notebook --- ...-27-performance-pandas-numpy-bottleneck.md | 7 + .../notebooks/performance_np_vs_pd.ipynb | 845 ++++++++++++++++++ 2 files changed, 852 insertions(+) create mode 100644 content/articles/2018-03-27-performance-pandas-numpy-bottleneck.md create mode 100644 content/downloads/notebooks/performance_np_vs_pd.ipynb diff --git a/content/articles/2018-03-27-performance-pandas-numpy-bottleneck.md b/content/articles/2018-03-27-performance-pandas-numpy-bottleneck.md new file mode 100644 index 00000000..9e083269 --- /dev/null +++ b/content/articles/2018-03-27-performance-pandas-numpy-bottleneck.md @@ -0,0 +1,7 @@ +--- +title: Performance: Pandas vs NumPy (vs Bottleneck) +date: 2018-03-27T18:02:36+00:00 +author: Maximiliano Greco +slug: performance-pandas-numpy-bottleneck + +{% notebook downloads/notebooks/performance_np_vs_pd.ipynb cells[:] %} diff --git a/content/downloads/notebooks/performance_np_vs_pd.ipynb b/content/downloads/notebooks/performance_np_vs_pd.ipynb new file mode 100644 index 00000000..71b9dc17 --- /dev/null +++ b/content/downloads/notebooks/performance_np_vs_pd.ipynb @@ -0,0 +1,845 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introducción\n", + "\n", + "Hace un par de semanas salío un [proyecto muy interesante](https://github.com/mm-mansour/Fast-Pandas) en el que se muestra cómo funciona `Pandas`, en cuanto a performance, comparado con `NumPy`. Bien, dado que uso `Pandas` y `NumPy` a diario no me costó encontrar algunas cosas (algo difusas) que estaría bien comentar cuando vemos comparaciones de este estilo, ya que nos podemos quedar con alguna que otar idea errónea.\n", + "\n", + "Voy a tratar de explicar cómo funciona `Pandas` y `NumPy` cuando hácemos llamadas del estilo:\n", + "\n", + " In [0]: d = pd.DataFrame(...)\n", + " In [1]: d.sum()\n", + " In [2]: np.sum(d)\n", + "\n", + "De hecho, aunque nos pueda parecer trivial, cuando buscamos la máxima performance esto toma especial releevancia, y desde luego no es lo mismo realizar una llamada como la `d.sum()` u otra(s) como la `np.sum(d)`. Otro apunte intersante, para los que no lo sabéis, los cálculos en`Pandas` no solo se apoyan en `NumPy`, también usan [`bottleneck`](https://github.com/kwgoodman/bottleneck) (y en el futuro quizá otras librerías), es por ello que este último es usado en las comparaciones, lo veremo con mas detalle." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Dependencias\n", + "# !pip install git+https://github.com/mmngreco/timer_plus.git\n", + "# !pip install watermark\n", + "# !pip install bottleneck" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "from matplotlib import pyplot as plt\n", + "import seaborn as sns\n", + "import numpy as np\n", + "import pandas as pd\n", + "import bottleneck as bn\n", + "import timer_plus as tp" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Descripción del entorno y SO\n", + "\n", + "Esto siempre es un factor a tener en cuenta cuando se habla de performance." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext watermark" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPython 3.6.4\n", + "IPython 6.2.1\n", + "\n", + "numpy 1.14.1\n", + "scipy 1.0.0\n", + "pandas 0.22.0\n", + "bottleneck 1.2.1\n", + "numexpr 2.6.4\n", + "\n", + "compiler : GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)\n", + "system : Darwin\n", + "release : 17.4.0\n", + "machine : x86_64\n", + "processor : i386\n", + "CPU cores : 4\n", + "interpreter: 64bit\n" + ] + } + ], + "source": [ + "%watermark -v -m -p numpy,scipy,pandas,bottleneck,numexpr" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def get_data(constructor, shape, dtype):\n", + " arr = np.random.randint(0, 1000, shape).astype(dtype)\n", + " return constructor(arr)\n", + "\n", + "def timeit_results(name, stmt, data_name, repeats, globs):\n", + " \"\"\"Calcula el tiempo de un statement.\n", + " \n", + " Parameters\n", + " ----------\n", + " name : str\n", + " Nombre que se le quiere dar al stmt.\n", + " stmt : str\n", + " Statement que se ejecutará.\n", + " data_name : str\n", + " Nombre de la variable que se usa como input en el \n", + " stmt.\n", + " repeats : int\n", + " Número de repeticiones que se ejecutará el stmt.\n", + " globs : dict\n", + " Diccionario con todas la variables necesarias para \n", + " que el stmt se pueda ejecutar.\n", + " \n", + " Returns\n", + " -------\n", + " time_results : pandas.DataFrame\n", + " Devuelve un pandas.DataFrame en formato long table \n", + " con columnas: time, stmt, N, name. La longitud será\n", + " igual a repeats.\n", + " \"\"\"\n", + " res = pd.DataFrame(tp.Timer(stmt, globals=globs).repeat_autorange(repeats), columns=['time'])\n", + " res['stmt'] = stmt\n", + " res['N'] = \"{:.0e}\".format(globs[data_name].shape[0]).replace('e+0', '0^')\n", + " res['name'] = name\n", + " return res" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Calculando tiempos" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "En esta parte voy a construir un `DataFrame` en que se guardan los tiempos (nombre, statement, N) de ejecución de las siguiente llamadas:\n", + "\n", + " >>> pandas_stmt = \"d.sum()\"\n", + " >>> numpy_stmt = \"np.nansum(d)\"\n", + " >>> pure_numpy_stmt = \"pd.Series(np.nansum(d.values), index=d.columns)\"\n", + " >>> bottleneck_stmt = \"pd.Series(bn.nansum(d.values), index=d.columns)\"\n", + "\n", + "Estas llamadas se explican con más detalle más abajo, de momento, vamos a quedarnos con que son algunas de las posibles alternativas para calcular la `sum` de un `DataFrame` y que nos devuelen el mismo objeto en los 4 casos.\n", + "\n", + "Para realizar la comparación de tiempos he usado la función `timeit_results` definida arriba, esta función usa un mini proyecto para poder hacer este tipo de comparaciones de forma más cómoda.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# CALCULO DE TIEMPOS\n", + "# ==================\n", + "\n", + "# Definimos las longitudes cómo potencias de 10 hasta 10^8\n", + "N_list = np.power(10, np.arange(1, 9))\n", + "repeats = 30\n", + "dtype = np.int64\n", + "data = get_data(pd.DataFrame, (max(N_list),1), dtype)\n", + "\n", + "# Statements\n", + "pandas_stmt = \"d.sum()\"\n", + "numpy_stmt = \"np.sum(d)\"\n", + "pure_numpy_stmt = \"pd.Series(np.nansum(d.values), index=d.columns)\"\n", + "bottleneck_stmt = \"pd.Series(bn.nansum(d.values), index=d.columns)\"\n", + "\n", + "# Variables usadas en los statements\n", + "globs = {\"d\": data, \"np\": np, \"pd\": pd, \"data\": data, \"bn\": bn}\n", + "\n", + "results = []\n", + "for N in N_list:\n", + " globs['d'] = data[:N:]\n", + " results += [timeit_results('pandas', pandas_stmt, 'd', repeats, globs)]\n", + " results += [timeit_results('numpy', numpy_stmt, 'd', repeats, globs)]\n", + " results += [timeit_results('pure_numpy', pure_numpy_stmt, 'd', repeats, globs)]\n", + " results += [timeit_results('botleneck', bottleneck_stmt, 'd', repeats, globs)]\n", + "\n", + "pd_results = pd.concat(results)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Los statements los he escrito de tal forma que devuelvan el mismo objeto, un `pandas.Series` en este caso que es lo devuelve `d.sum()`. Así, estamos asegurando que el usuario de `pandas` tiene cómo resultado el mismo objeto y los tiempos son comparables entre sí." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
timestmtNname
00.000266d.sum()10^1pandas
10.000208d.sum()10^1pandas
20.000209d.sum()10^1pandas
30.000206d.sum()10^1pandas
40.000198d.sum()10^1pandas
\n", + "
" + ], + "text/plain": [ + " time stmt N name\n", + "0 0.000266 d.sum() 10^1 pandas\n", + "1 0.000208 d.sum() 10^1 pandas\n", + "2 0.000209 d.sum() 10^1 pandas\n", + "3 0.000206 d.sum() 10^1 pandas\n", + "4 0.000198 d.sum() 10^1 pandas" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd_results.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Resultados\n", + "\n", + "Con el `DataFrame` en [long form](https://medium.com/@andykashyap/how-to-convert-a-table-into-long-form-or-tidy-form-for-seaborn-visualizations-2bd8b44cdc29) anterior voy a construir una tabla resumen de los tiempos medios y desviación típica por longitud del `DataFrame` y nombre (un alias del `statement`)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def mu_std(x):\n", + " \"\"\"Imprime la media (std).\n", + " \n", + " Sólo funciona en python36\n", + " \n", + " Parameters\n", + " ----------\n", + " x : array-like\n", + " \n", + " Returns\n", + " -------\n", + " mu_std : str\n", + " \"\"\"\n", + " mu = np.mean(x)\n", + " std = np.std(x)\n", + " return f\"{mu:.6f} ({std:.6f})\"" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namebotlenecknumpypandaspure_numpy
N
10^10.000094 (0.000003)0.000208 (0.000009)0.000203 (0.000014)0.000109 (0.000005)
10^20.000096 (0.000005)0.000227 (0.000036)0.000195 (0.000006)0.000112 (0.000005)
10^30.000096 (0.000004)0.000219 (0.000010)0.000206 (0.000010)0.000112 (0.000006)
10^40.000107 (0.000004)0.000262 (0.000008)0.000244 (0.000009)0.000141 (0.000015)
10^50.000167 (0.000007)0.000693 (0.000013)0.000677 (0.000015)0.000273 (0.000007)
10^60.001121 (0.000047)0.008130 (0.000179)0.008092 (0.000125)0.004545 (0.000105)
10^70.008969 (0.000172)0.075049 (0.001520)0.080686 (0.027643)0.045551 (0.001036)
10^80.084068 (0.001555)1.227781 (0.067859)1.463971 (0.755026)0.893592 (0.021614)
\n", + "
" + ], + "text/plain": [ + "name botleneck numpy pandas \\\n", + "N \n", + "10^1 0.000094 (0.000003) 0.000208 (0.000009) 0.000203 (0.000014) \n", + "10^2 0.000096 (0.000005) 0.000227 (0.000036) 0.000195 (0.000006) \n", + "10^3 0.000096 (0.000004) 0.000219 (0.000010) 0.000206 (0.000010) \n", + "10^4 0.000107 (0.000004) 0.000262 (0.000008) 0.000244 (0.000009) \n", + "10^5 0.000167 (0.000007) 0.000693 (0.000013) 0.000677 (0.000015) \n", + "10^6 0.001121 (0.000047) 0.008130 (0.000179) 0.008092 (0.000125) \n", + "10^7 0.008969 (0.000172) 0.075049 (0.001520) 0.080686 (0.027643) \n", + "10^8 0.084068 (0.001555) 1.227781 (0.067859) 1.463971 (0.755026) \n", + "\n", + "name pure_numpy \n", + "N \n", + "10^1 0.000109 (0.000005) \n", + "10^2 0.000112 (0.000005) \n", + "10^3 0.000112 (0.000006) \n", + "10^4 0.000141 (0.000015) \n", + "10^5 0.000273 (0.000007) \n", + "10^6 0.004545 (0.000105) \n", + "10^7 0.045551 (0.001036) \n", + "10^8 0.893592 (0.021614) " + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd_results.pivot_table(index='N', values='time', columns=['name'], aggfunc=mu_std)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> _Tabla 1: Tiempos por statement y longitud del DataFrame._\n", + "\n", + "La tabla muestra la media de los tiempos en segundos de cada statement, entre paréntesis las desviaciones típicas. Como una imagen vale más que mil palabras, que hable `seaborn`:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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ffr0tsOHDw9PSkqKCg0NjX/99dcrjq+yv8vU1FS/Pn36dIqLi4tJSEiIOnDgQEMAuO+++yKMPQfeeeedFoMHD+5QVT2MioqKZODAgbdGRkbGPvTQQ7deu3ZNLG135MgRv169ekVGRUXFxsbGxqSmpvqVl5fj6aefDjX2Dvn4449v6p0xZ86c5o899lg74+t77rmn48aNGwMBrW1Mnjw5JC4uLqZXr16RW7ZsCTD+LpYtW9bE+P5+/fpF9OnTp1P79u07P/PMM6GAFngYOnRouHHfr732WkvANectQAvu5D4zOeLM/7wZXpKe3qjs3LkGJenpjc78z5vhuU8/E6FKee4CgG+//TYwISEhKjw8vPOKFSuq/IzN8dx1I567eO4iorqBAZ56bMnOnObbj52zeBG3/di5pkt35dzULdhalX0hG3399deNo6OjY6Ojo2NjYmJiCwoKvKq6uAgJCYmfOnVqSNeuXaM7d+4c89NPPwX07t27U1hYWOe333472KTcoKFDhxYaXw8YMODiZ599VrtduHd/3BzZP1j8vSL7h6bYs9Du3ysA/P777w2nTZuWn5WVldqkSZOyJUuWBAHA1atXvQ4cOJAxZ86cnEmTJt10QbNo0aJm9913X2FGRkZaenp6ao8ePap9clZYWOizc+fOo2+++WbuyJEjO/31r389c+zYsdSMjAz/HTt2+APAvn37Gt95551XAO0i8Msvv2yWkpKStnHjxqxDhw41Mi0vPj7+6g8//NC4Jsdvi5UZK5vvOLnD4mex4+SOpqsyVtn9WRw/frzhM888c/bo0aNpgYGB5e+8807w1atXZerUqeHJyclZe/bsyczPz/e19N7x48df+O6775pGR0fH/td//Vfozz//7A8Ap06d8pk9e3abbdu2HU1LS0vv3r371VmzZlV0R2/YsGH5vn37MidNmlRgXFZSUiLTpk1r99VXX2WnpqamP/744+dmzJgRAgBz5sxpfeTIkbSjR4+mLV68OAcAtmzZ0vi222674bM/c+aM7969ezO++uqrY6+++mqIcXlKSkqjDz74IDczMzP1+PHjfsa2Zs5gMEhKSkr6W2+9lTtz5sy2ANC2bVvD9u3bj6alpaWvWrXq1+eee67iwjY9Pd1/3rx5uVlZWam///6737ffftv4zJkz3t98803QsWPHUo8ePZo2e/bsU9V9BhkZGf4LFizITU9PT12zZk3zo0ePNkxJSUkfP378uffee68lAGzfvr1x9+7dK4534sSJ4e+///7vBw8ezKis3HHjxl1Yu3ZtM+Prr776KmjcuHEFAQEB5V9//XVWWlpa+o8//nj073//e6jxBq86tn5OAJCYmHhl+/btNv29ZGVlNfzxxx+P7tmzJ/3dd99tW1JSIlX9XT711FPt58+f/3tqamr6O++8kzd58uR2ALB48eKct99+u82mTZsaz5s3r/XHH3/8OwC8/PLLrYznbtN/xqEW7777bkt/f//yo0ePpr3yyiun0tLSGlmq55gxYzo888wz+ZmZmWl79+7NaNeuXemSJUuapqSk+Kenp6d+//33R1955ZVQ442kNYqLi73uueeey6mpqemNGjUq++///u+Q7du3H/3iiy+yZs2aVdGu09LSAtavX/9renp6anJyclBWVpbvzp07A06dOuVrbH9Tpkw5b9y+ts9bAFCwfHnzKz//bPHcdeXnn5sWrFjBcxeA3Nxcv927d2du2LDh2LPPPtv+6tWrAlj+jC0dD89d1eO560Y8dxGRu2NUtx5btz8vuKr1a/fltXjyzg7nq9rGEtMv5NLSUnTt2jW2W7duN1yUvffee63nzJmT069fvyuFhYVeAQEB1V5phIWFXT948GDGxIkTw5588snwX375JaO4uNirc+fOcS+88MLZjIyMBk2aNDH4+/tXJJju0aPHlTfffLMNAKcPD6twaEWVv1ccWtECPSfb/Hs1CgkJKenVq1cxAHTr1u3q8ePH/QBgzJgxFwBgwIABRUVFRV7nzp3zbtGiRUWXi549e155+umnw0tLS72GDRtWYCyjKg899NBFLy8vdO/e/Wrz5s1Lk5KSigEgMjKyODs7269Xr17FhYWFPkFBQeWAdgH+4IMPXjR2y+7Xr99F0/KCg4MNJ06cqLUeVcnZyVV+FsnZyS3GxY6z67No3br19X79+l0BgPHjx5+fM2dOy4MHD14KDQ0tiY+PLwGAsWPHnl+4cOFNdYiIiCjNyso6smHDhsDvv//+lgcffDBqyZIl2VevXvXKzs5umJSUFA0ApaWlkpCQUGR832OPPVZgXtbhw4f9jh075n/vvfdGAlpOpODg4FIAiIqKKn7kkUc6DB48+OLYsWMvAtoT+ODgYINpGYMHD77o7e2NhISEa+fPn6+4KI2P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vYs+//zzZgCwZ8+ehkePHr0pH1izZs3KW7dufX3p0qVNAS2wfPnyZa+777778po1a5oZDAacPHnSZ/fu3Y379Olzxez3ez01NTWgrKwMWVlZvocPH7Y4jMJWp06d8tFniLr4+uuvn0hJSamod22ftwDAR+8hURnfli157gKwbt26oLKyMqSmpvrl5ub6denS5Zq9vxcjnrt47uK5i4g8GQM89dij3UPPVrV+aELoOXvKre4mH9CS4yUlJRW/8cYbp+Pj468cOXKkYWUXF9aKj48vMe+KmpaW5hcVFVW7X25dRlf5e0WX0Xb9XqsTFBRU1q1bt+ipU6e2/+ijj44D2jSkI0eObA8AmzdvDoyNjY2LiYmJ/eqrr4JeeOGFMwDw2muvnRgyZEjHO+64I6pVq2qCUxb069ev8D//+U8gAPTu3fvqI488cqFz585xAwcOjEhKSqrool9SUiLHjx/3u+uuu65UXppjDY4YXOVnMThisN2fxa233npt0aJFzSMjI2MLCgp8ZsyYcTYgIED93//9X87AgQM7JiQkRIWFhVXchJl+FqdPn/Z56KGHOnbq1CkuOjo6zsfHBy+++GJ+27ZtDR999NHxUaNG3RoZGRmbkJAQnZKS0rCqejRs2FCtXLky+8UXXwyNioqKjYuLi/3xxx8bGwwGGTNmTAd9KuPYp59++kyLFi3KHnjggaLU1NQAa/IvdO3atej5558PjYyMjGvXrl3J+PHjLwLAyJEj21c3feyzzz6bv2LFiuZdunSJPnr0aMPqniJevHjRu3///p0iIyNj+/TpE/X666/nAsCf//znszt27AiMj4+P2bVrVyNbn0Y+/PDDhTt27KiYKvaTTz45Pm3atHZdu3aNNh3Oefz4cd+77767IgdYYmLitStXrni1atXqunEoxVNPPXXh0KFDjTp37hzz+eefN+vQocNNN5MdO3YsHTRoUEFMTEzcsGHDOsTFxV0FbP+cAGDbtm2BDz/8cCEAZGVl+d1yyy1V32lWoqq/yxUrVvz66aeftoiKiort1KlT3Nq1a5sWFxfLM888E75w4cLj4eHhpbNnz859/PHHw61pMzNmzMi/cuWKd2RkZOzs2bNbx8fHV/y9m7abzz///Ld58+a1jIyMjE1MTIzOzc31GT9+/MW4uLjimJiYuL59+0a+9tpreeZDSO6///6isLCwkqio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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "g = sns.factorplot(x='name', y='time', data=pd_results, hue='stmt', size=4, col='N', sharey=False, sharex=False, aspect=0.5, kind='point', legend=False)\n", + "for ax in g.axes.flat:\n", + " labels = ax.get_xticklabels()\n", + " for l in labels:\n", + " l.update(dict(rotation=90))\n", + " ax.grid(True)\n", + "plt.legend(bbox_to_anchor=(0.1,1.03), loc=\"lower left\", bbox_transform=g.fig.transFigure, ncol=4)\n", + "g.fig.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> _Gráfico 1: Scatter de la media de los tiempos de ejecución por statement y tamaño del DataFrame._\n", + "\n", + "> Nota: Recordemos que `d` es un `pandas.DataFrame`. \n", + "\n", + "El gráfico 1 muestra en el eje de las absisas los nombres de los statements y en el eje de las ordenadas los tiempos en segundos. Los gráficos están separados por tamaño del `DataFrame` dado que hay mucha diferencia entre los tiempos extremos y por tanto, si los pintamos en un mismo gráfico no podríamos apreciar los detalles de cada caso. \n", + "\n", + "Lo primero que resalta del gráfico 1, son los tiempos obtenidos para los casos que he llamado __pandas__ y __numpy__, los cuales contrastan con los nombrados __pure_numpy__ y __bottleneck__.\n", + "\n", + "Si vamos un poco más al detalle, __numpy__ (con el statement `np.nansum(d)`), es más lenta que la versión __pandas__, `d.sum()`, sin embargo, numpy va alcanzando a pandas a medida que aumenta N, hasta que se invierten las posiciones a partir del tamaño N=10^6, eso sí, pero no tan rápido esto sólo en media, están muy cerca cómo para afirmarlo rotundamente, lo que sí podemos decir es que los tiempos varían mucho más en el caso pandas y esto se aprecia en las barras de error del caso pandas.\n", + "\n", + "Por otra parte, en las primeras posiciones tenemos las implementaciones __pure_numpy__ y __bottleneck__, son con diferencia mucho más rápidas que las anteriores, la pega está en que las implementaciones no son tan \"high level\" como las otras, tenemos que acceder al `values`, llamar a la función correspondiente en numpy/bottleneck y luego envolverlo todo en Pandas. Es curioso cómo el caso pure_numpy se aleja de bottleneck a medida que aumenta N, especialmente notorio a partird e N=10^5.\n", + "\n", + "En pandas son muy listos al usar bottleneck siempre que sea posible, porque fijaros que al final la \"lucha\" está entre NumPy y Bottleneck." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Interpretando los statements:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "import inspect\n", + "\n", + "def print_code(func, no_lines=None):\n", + " \"\"\"Imprime el código de la función o modulo.\n", + " \n", + " Parameters\n", + " ----------\n", + " func : function or module\n", + " no_lines : list\n", + " Una lista de enteros.\n", + " \n", + " Returns\n", + " -------\n", + " None\n", + " \"\"\"\n", + " print('file:',inspect.getfile(func))\n", + " lines = inspect.getsourcelines(func)\n", + " for i, line in enumerate(lines[0]):\n", + " if no_lines is not None:\n", + " if i not in no_lines:\n", + " continue\n", + " print(i, '\\t', line.strip('\\n'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Caso Pandas\n", + "\n", + "La lógica de `Pandas` es más complicada, y no es tan inmediato de ver en el código, cómo en el caso de `NumPy` (explicado en el siguiente apartado). Lo podríamos resumir en lo siguiente:\n", + "\n", + " >>> d = pd.DataFrame(...)\n", + " >>> d.sum() --> np.nansum(d.values) --> d.values.sum()\n", + " ^^^\n", + " Pandas llama a numpy.nansum (o bottleneck.nansum) en nanops.py\n", + "\n", + "Cuando invocamos al método `.sum()` de pandas, por defecto tanto las `Series` como los `DataFrames` usan la función `nanops.nansum` de `Pandas` que heredan de la clase `NDFrame`:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "file: /Users/mmngreco/miniconda3/lib/python3.6/site-packages/pandas/core/generic.py\n", + "97 \t class NDFrame(PandasObject, SelectionMixin):\n", + "6990 \t cls.sum = _make_min_count_stat_function(\n", + "6991 \t cls, 'sum', name, name2, axis_descr,\n", + "6992 \t 'Return the sum of the values for the requested axis',\n", + "6993 \t nanops.nansum, _sum_examples)\n" + ] + } + ], + "source": [ + "print_code(pd.core.generic, [97,6990, 6991, 6992, 6993])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Aquí puede verse que en la clase de la que heredan las Series y los DataFrames se define el método `sum` cómo un alias de `nanops.nansum`:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "file: /Users/mmngreco/miniconda3/lib/python3.6/site-packages/pandas/core/nanops.py\n", + "0 \t @disallow('M8')\n", + "1 \t @bottleneck_switch()\n", + "2 \t def nansum(values, axis=None, skipna=True, min_count=0):\n", + "3 \t values, mask, dtype, dtype_max = _get_values(values, skipna, 0)\n", + "4 \t dtype_sum = dtype_max\n", + "5 \t if is_float_dtype(dtype):\n", + "6 \t dtype_sum = dtype\n", + "7 \t elif is_timedelta64_dtype(dtype):\n", + "8 \t dtype_sum = np.float64\n", + "9 \t the_sum = values.sum(axis, dtype=dtype_sum)\n", + "10 \t the_sum = _maybe_null_out(the_sum, axis, mask, min_count=min_count)\n", + "11 \t \n", + "12 \t return _wrap_results(the_sum, dtype)\n" + ] + } + ], + "source": [ + "print_code(pd.core.nanops.nansum)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lo que hace esta función, `pandas.core.nanops.nansum`, es comprobar si es posible usar `bottleneck` (ver decorador línea 1) y si no, llama al método `sum` de `values`, es decir, llama a `numpy.nansum` (ver línea 9) (ver línea 6993 de `pandas.core.generic`) y nos devuelve el resultado." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Caso numpy\n", + "\n", + " numpy_stmt : np.sum(d)\n", + "\n", + "¿Qué hace `numpy` en este caso? Veamos el código:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "file: /Users/mmngreco/miniconda3/lib/python3.6/site-packages/numpy/core/fromnumeric.py\n", + "0 \t def sum(a, axis=None, dtype=None, out=None, keepdims=np._NoValue):\n", + "87 \t kwargs = {}\n", + "88 \t if keepdims is not np._NoValue:\n", + "89 \t kwargs['keepdims'] = keepdims\n", + "90 \t if isinstance(a, _gentype):\n", + "91 \t res = _sum_(a)\n", + "92 \t if out is not None:\n", + "93 \t out[...] = res\n", + "94 \t return out\n", + "95 \t return res\n", + "96 \t if type(a) is not mu.ndarray:\n", + "97 \t try:\n", + "98 \t sum = a.sum\n", + "99 \t except AttributeError:\n", + "100 \t pass\n", + "101 \t else:\n", + "102 \t return sum(axis=axis, dtype=dtype, out=out, **kwargs)\n", + "103 \t return _methods._sum(a, axis=axis, dtype=dtype,\n", + "104 \t out=out, **kwargs)\n" + ] + } + ], + "source": [ + "print_code(np.sum, no_lines=[0, *range(87,105)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Aquí la clave está en las líneas 96-98: primero `numpy.sum` comprueba si el objeto que ha recibido NO ES un `ndarray` (línea 96), si no lo es, intenta acceder al método `.sum()` del objeto. En nuestro caso, el objeto es un `pandas.DataFrame` y por tanto entra en el condicional (no es un `ndarray`) y llama al `d.sum()` que tenga implementado `Pandas`. \n", + "\n", + "Algo como lo siguiente:\n", + "\n", + " >>> d = pd.DataFrame(...)\n", + " >>> np.sum(d) --> d.sum() --> np.nansum(d.values) --> d.values._sum()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Caso pure_numpy\n", + "\n", + " pure_numpy_stmt : pd.Series(np.nansum(d.values), index=d.columns)\n", + "\n", + "El caso pure_numpy es lo que se diría _\"ir al grano\"_. En este caso sabemos lo que hacen las librerías y se lo ponemos fácil:\n", + " \n", + " >>> d = pd.DataFrame\n", + " >>> pd.Series(\n", + " np.nansum(d.values), # en numpy directamente\n", + " index=d.columns\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Caso bottleneck\n", + "\n", + " bottleneck_stmt : pd.Series(bn.nansum(d.values), index=d.columns)\n", + "\n", + "Este paso, ahorra muchas llamadas intermedias si pandas efectivamente puede usar `bottleneck`.\n", + "\n", + " >>> d = pd.DataFrame\n", + " >>> pd.Series(\n", + " bn.nansum(d.values), # en bottleneck directamente\n", + " index=d.columns\n", + " )\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> Nota: Dado que sabemos que no tenemos `NaNs` podríamos ir un paso más allá y aplicar la `np.sum`, pero estaríamos siendo injustos con `Pandas`.\n", + "\n", + "> Nota 2: En el caso de la suma, pandas no usa bottleneck (más detalles en el siguiente apartado)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# El papel de bottleneck en Pandas\n", + "\n", + "Como hemos comentado antes, `Pandas` usa para algunos cálculos `bottleneck` en lugar de `numpy`, esto es debido a que `bottleneck` tiene un conjunto de funciones que gestionan `NaNs` implementadas en C, lo cual le dota de una gran velocidad de cálculo. Las funciones que hacen uso de bottleneck, siempre y cuando sea posible, son las siguientes:\n", + "\n", + " - mean\n", + " - median\n", + " - std\n", + " - var\n", + " - min\n", + " - max\n", + "\n", + "Esto lo podemos ver en `pandas.core.nanops`, todas la funciones que usan el decorador `@bottleneck_switch` intentarán usar `bottleneck` para realizar la operación." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Si tenéis curiosidad por este decorador os lo dejo por aquí:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "file: /Users/mmngreco/miniconda3/lib/python3.6/site-packages/pandas/core/nanops.py\n", + "89 \t class bottleneck_switch(object):\n", + "118 \t if (_USE_BOTTLENECK and skipna and\n", + "119 \t _bn_ok_dtype(values.dtype, bn_name)):\n", + "120 \t result = bn_func(values, axis=axis, **kwds)\n", + "121 \t \n", + "122 \t # prefer to treat inf/-inf as NA, but must compute the func\n", + "123 \t # twice :(\n", + "124 \t if _has_infs(result):\n", + "125 \t result = alt(values, axis=axis, skipna=skipna, **kwds)\n", + "126 \t else:\n", + "127 \t result = alt(values, axis=axis, skipna=skipna, **kwds)\n", + "128 \t except Exception:\n", + "129 \t try:\n", + "130 \t result = alt(values, axis=axis, skipna=skipna, **kwds)\n", + "131 \t except ValueError as e:\n", + "132 \t # we want to transform an object array\n", + "133 \t # ValueError message to the more typical TypeError\n", + "134 \t # e.g. this is normally a disallowed function on\n", + "135 \t # object arrays that contain strings\n", + "136 \t \n", + "137 \t if is_object_dtype(values):\n", + "138 \t raise TypeError(e)\n", + "139 \t raise\n", + "140 \t \n", + "141 \t return result\n", + "142 \t \n", + "143 \t return f\n" + ] + } + ], + "source": [ + "print_code(pd.core.nanops, [89,*range(118,144)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ahora ya sabéis qué hace Pandas y NumPy por debajo cuando se los llama como hemos visto. Pandas, a medida que aumenta el tamaño de los datos, se vuelve más eficiente, sin embargo, en ningún caso, gana a una implementación \"ad hoc\" como las pure_numpy o bottleneck. Siempre se puede atajar, pero Pandas lo hace muy bien en general, no nos tenemos que preocupar de si lo que devuelve es una Serie, DataFrame, array, int, float, etc, nos devuelve lo que tiene que ser y sin preocuparnos por los indices, columnas, nombres, dtypes... por lo que, no está nada mal. \n", + "\n", + "That's all folks!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From d9457725985bc8de47ae28279f3bfa95d7272dc6 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Wed, 18 Apr 2018 18:45:46 +0200 Subject: [PATCH 2/3] Fixed typos and grammar --- .../notebooks/performance_np_vs_pd.ipynb | 109 ++++++++---------- 1 file changed, 51 insertions(+), 58 deletions(-) diff --git a/content/downloads/notebooks/performance_np_vs_pd.ipynb b/content/downloads/notebooks/performance_np_vs_pd.ipynb index 71b9dc17..59e81a8c 100644 --- a/content/downloads/notebooks/performance_np_vs_pd.ipynb +++ b/content/downloads/notebooks/performance_np_vs_pd.ipynb @@ -6,15 +6,15 @@ "source": [ "# Introducción\n", "\n", - "Hace un par de semanas salío un [proyecto muy interesante](https://github.com/mm-mansour/Fast-Pandas) en el que se muestra cómo funciona `Pandas`, en cuanto a performance, comparado con `NumPy`. Bien, dado que uso `Pandas` y `NumPy` a diario no me costó encontrar algunas cosas (algo difusas) que estaría bien comentar cuando vemos comparaciones de este estilo, ya que nos podemos quedar con alguna que otar idea errónea.\n", + "Hace varias de semanas salió un [proyecto muy interesante](https://github.com/mm-mansour/Fast-Pandas) en el que se compara la performance de `Pandas` con `NumPy`. Bien, dado que uso `Pandas` y `NumPy` a diario no me costó demasiado encontrar algunas cosas (quizá algo difusas) que estarían bien comentar o matizar. Sobre todo cuando vemos comparaciones de este estilo, ya que nos podemos quedar con alguna que otra idea errónea.\n", "\n", - "Voy a tratar de explicar cómo funciona `Pandas` y `NumPy` cuando hácemos llamadas del estilo:\n", + "A continuación voy a tratar de explicar cómo funciona `Pandas` y `NumPy` cuando hacemos llamadas del estilo:\n", "\n", " In [0]: d = pd.DataFrame(...)\n", " In [1]: d.sum()\n", " In [2]: np.sum(d)\n", "\n", - "De hecho, aunque nos pueda parecer trivial, cuando buscamos la máxima performance esto toma especial releevancia, y desde luego no es lo mismo realizar una llamada como la `d.sum()` u otra(s) como la `np.sum(d)`. Otro apunte intersante, para los que no lo sabéis, los cálculos en`Pandas` no solo se apoyan en `NumPy`, también usan [`bottleneck`](https://github.com/kwgoodman/bottleneck) (y en el futuro quizá otras librerías), es por ello que este último es usado en las comparaciones, lo veremo con mas detalle." + "De hecho, aunque nos pueda parecer trivial, cuando buscamos la máxima performance hacerlo de una u otra forma toma especial relevancia. Como podéis intuir, no es lo mismo hacer `d.sum()` que `np.sum(d)`. Otro apunte intersante, para los que no lo sabéis, los cálculos en `Pandas` se apoyan en `NumPy`, pero también en [`bottleneck`](https://github.com/kwgoodman/bottleneck) (y en el futuro quizá otras librerías), por ese motivo lo incluyo en esta comparativa, lo veremos con más detalle." ] }, { @@ -110,7 +110,7 @@ " Parameters\n", " ----------\n", " name : str\n", - " Nombre que se le quiere dar al stmt.\n", + " Nombre que se le quiere dar al statement.\n", " stmt : str\n", " Statement que se ejecutará.\n", " data_name : str\n", @@ -127,9 +127,10 @@ " time_results : pandas.DataFrame\n", " Devuelve un pandas.DataFrame en formato long table \n", " con columnas: time, stmt, N, name. La longitud será\n", - " igual a repeats.\n", + " igual a `repeats`.\n", " \"\"\"\n", - " res = pd.DataFrame(tp.Timer(stmt, globals=globs).repeat_autorange(repeats), columns=['time'])\n", + " tmr = tp.Timer(stmt, globals=globs).repeat_autorange(repeats)\n", + " res = pd.DataFrame(tmr, columns=['time'])\n", " res['stmt'] = stmt\n", " res['N'] = \"{:.0e}\".format(globs[data_name].shape[0]).replace('e+0', '0^')\n", " res['name'] = name\n", @@ -147,17 +148,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "En esta parte voy a construir un `DataFrame` en que se guardan los tiempos (nombre, statement, N) de ejecución de las siguiente llamadas:\n", + "En esta parte voy a construir un `DataFrame` en el que se guardan los tiempos (,nombre, statement, N) de ejecución de las siguiente llamadas:\n", "\n", " >>> pandas_stmt = \"d.sum()\"\n", " >>> numpy_stmt = \"np.nansum(d)\"\n", " >>> pure_numpy_stmt = \"pd.Series(np.nansum(d.values), index=d.columns)\"\n", " >>> bottleneck_stmt = \"pd.Series(bn.nansum(d.values), index=d.columns)\"\n", "\n", - "Estas llamadas se explican con más detalle más abajo, de momento, vamos a quedarnos con que son algunas de las posibles alternativas para calcular la `sum` de un `DataFrame` y que nos devuelen el mismo objeto en los 4 casos.\n", + "Estas llamadas se explican con más detalle más abajo, de momento vamos a quedarnos con que son algunas de las posibles alternativas para calcular la `sum` de un `DataFrame` y que nos __devuelen el mismo objeto__ en los 4 casos.\n", "\n", - "Para realizar la comparación de tiempos he usado la función `timeit_results` definida arriba, esta función usa un mini proyecto para poder hacer este tipo de comparaciones de forma más cómoda.\n", - "\n" + "Para realizar la comparación de tiempos he usado la función `timeit_results` definida arriba, esta función usa un mini proyecto para poder hacer este tipo de comparaciones de forma más cómoda.\n" ] }, { @@ -169,7 +169,7 @@ "# CALCULO DE TIEMPOS\n", "# ==================\n", "\n", - "# Definimos las longitudes cómo potencias de 10 hasta 10^8\n", + "# Definimos las longitudes como potencias de 10 hasta 10^8\n", "N_list = np.power(10, np.arange(1, 9))\n", "repeats = 30\n", "dtype = np.int64\n", @@ -199,7 +199,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Los statements los he escrito de tal forma que devuelvan el mismo objeto, un `pandas.Series` en este caso que es lo devuelve `d.sum()`. Así, estamos asegurando que el usuario de `pandas` tiene cómo resultado el mismo objeto y los tiempos son comparables entre sí." + "Los statements (`stmt`) los he escrito de tal forma que devuelvan el mismo objeto, un `pandas.Series` en este caso dado que es lo que devuelve `d.sum()`. Así nos estamos asegurando de que el usuario de `pandas` tiene como resultado el mismo objeto y los tiempos son comparables entre sí." ] }, { @@ -292,13 +292,25 @@ "pd_results.head()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Donde:\n", + "\n", + "- *time* es el tiempo medio en segundos. \n", + "- *stmt* es la línea de código que se ejecutará. \n", + "- *N* es la longitud de los datos.\n", + "- *name* es un alias más amigable del statement. " + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Resultados\n", "\n", - "Con el `DataFrame` en [long form](https://medium.com/@andykashyap/how-to-convert-a-table-into-long-form-or-tidy-form-for-seaborn-visualizations-2bd8b44cdc29) anterior voy a construir una tabla resumen de los tiempos medios y desviación típica por longitud del `DataFrame` y nombre (un alias del `statement`)." + "Con el `DataFrame` en [long form](https://medium.com/@andykashyap/how-to-convert-a-table-into-long-form-or-tidy-form-for-seaborn-visualizations-2bd8b44cdc29) anterior voy a construir una tabla resumen de los tiempos medios y desviación típica para cada longitud del `DataFrame` pasado como input y un nombre (no es mas que un alias del `statement`)." ] }, { @@ -464,7 +476,7 @@ "source": [ "> _Tabla 1: Tiempos por statement y longitud del DataFrame._\n", "\n", - "La tabla muestra la media de los tiempos en segundos de cada statement, entre paréntesis las desviaciones típicas. Como una imagen vale más que mil palabras, que hable `seaborn`:" + "La tabla muestra la media de los tiempos en segundos de cada statement. Entre paréntesis se encuentran las desviaciones típicas. Dado que una imagen vale más que mil palabras, que hable `seaborn`:" ] }, { @@ -502,15 +514,13 @@ "\n", "> Nota: Recordemos que `d` es un `pandas.DataFrame`. \n", "\n", - "El gráfico 1 muestra en el eje de las absisas los nombres de los statements y en el eje de las ordenadas los tiempos en segundos. Los gráficos están separados por tamaño del `DataFrame` dado que hay mucha diferencia entre los tiempos extremos y por tanto, si los pintamos en un mismo gráfico no podríamos apreciar los detalles de cada caso. \n", + "El gráfico 1 muestra en el eje de las absisas los nombres de los statements y en el eje de las ordenadas los tiempos en segundos. Los gráficos están separados por tamaño del `DataFrame`, esto es debido a que hay mucha diferencia entre los tiempos en los extremos de N, y si los pintamos en un mismo gráfico no podríamos apreciar los detalles de cada caso.\n", "\n", - "Lo primero que resalta del gráfico 1, son los tiempos obtenidos para los casos que he llamado __pandas__ y __numpy__, los cuales contrastan con los nombrados __pure_numpy__ y __bottleneck__.\n", + "Lo primero que resalta del gráfico 1 son los tiempos de los casos llamados _pandas_ y _numpy_, los cuales contrastan con los *pure_numpy* y _bottleneck_. Si vamos un poco más al detalle, _numpy_ (con el statement `np.nansum(d)`), es más lenta que la versión _pandas_, `d.sum()`, sin embargo, _numpy_ va alcanzando a _pandas_ a medida que aumenta N, hasta el punto en el que se invierten las posiciones, esto ocurre a partir del tamaño N=10^6, esto sólo es en media. Los tiempos varían mucho más en el caso _pandas_, y se aprecia en las barras de error que se aprecian claramente para los tamaños grandes.\n", "\n", - "Si vamos un poco más al detalle, __numpy__ (con el statement `np.nansum(d)`), es más lenta que la versión __pandas__, `d.sum()`, sin embargo, numpy va alcanzando a pandas a medida que aumenta N, hasta que se invierten las posiciones a partir del tamaño N=10^6, eso sí, pero no tan rápido esto sólo en media, están muy cerca cómo para afirmarlo rotundamente, lo que sí podemos decir es que los tiempos varían mucho más en el caso pandas y esto se aprecia en las barras de error del caso pandas.\n", + "Por otra parte, las implementaciones *pure_numpy* y _bottleneck_ se encuentran en las primeras posiciones del ranking de performance, que son con diferencia mucho más rápidas que las anteriores. La pega está en que las implementaciones no son tan \"high level\" como las otras, tenemos que acceder al `values`, llamar a la función correspondiente en `numpy`/`bottleneck` y luego envolverlo todo en `Pandas`. Es curioso como el caso *pure_numpy* se aleja de _bottleneck_ a medida que aumenta N, especialmente notorio a partir de N=10^5.\n", "\n", - "Por otra parte, en las primeras posiciones tenemos las implementaciones __pure_numpy__ y __bottleneck__, son con diferencia mucho más rápidas que las anteriores, la pega está en que las implementaciones no son tan \"high level\" como las otras, tenemos que acceder al `values`, llamar a la función correspondiente en numpy/bottleneck y luego envolverlo todo en Pandas. Es curioso cómo el caso pure_numpy se aleja de bottleneck a medida que aumenta N, especialmente notorio a partird e N=10^5.\n", - "\n", - "En pandas son muy listos al usar bottleneck siempre que sea posible, porque fijaros que al final la \"lucha\" está entre NumPy y Bottleneck." + "Fijaos que claro, en `Pandas` son muy listos al usar `Bottleneck` siempre que sea posible, porque al final la \"lucha\" está entre `NumPy` y `Bottleneck`." ] }, { @@ -556,14 +566,14 @@ "source": [ "## Caso Pandas\n", "\n", - "La lógica de `Pandas` es más complicada, y no es tan inmediato de ver en el código, cómo en el caso de `NumPy` (explicado en el siguiente apartado). Lo podríamos resumir en lo siguiente:\n", + "La lógica de `Pandas` es más complicada, y no es tan inmediato de ver en el código como en el caso de `NumPy` (explicado en el siguiente apartado). Lo podríamos resumir en lo siguiente:\n", "\n", " >>> d = pd.DataFrame(...)\n", " >>> d.sum() --> np.nansum(d.values) --> d.values.sum()\n", " ^^^\n", " Pandas llama a numpy.nansum (o bottleneck.nansum) en nanops.py\n", "\n", - "Cuando invocamos al método `.sum()` de pandas, por defecto tanto las `Series` como los `DataFrames` usan la función `nanops.nansum` de `Pandas` que heredan de la clase `NDFrame`:" + "Cuando invocamos al método `.sum()` de `Pandas`, por defecto, tanto las `Series` como los `DataFrames`, usan la función `nanops.nansum` de `Pandas` que heredan de la clase `NDFrame`:" ] }, { @@ -592,7 +602,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Aquí puede verse que en la clase de la que heredan las Series y los DataFrames se define el método `sum` cómo un alias de `nanops.nansum`:" + "Arriba puede verse que en la clase `NDFrame` se define el método `sum` como un alias de `nanops.nansum` en la última línea y `nanops.nansum` hace lo siguiente:" ] }, { @@ -685,7 +695,7 @@ "source": [ "Aquí la clave está en las líneas 96-98: primero `numpy.sum` comprueba si el objeto que ha recibido NO ES un `ndarray` (línea 96), si no lo es, intenta acceder al método `.sum()` del objeto. En nuestro caso, el objeto es un `pandas.DataFrame` y por tanto entra en el condicional (no es un `ndarray`) y llama al `d.sum()` que tenga implementado `Pandas`. \n", "\n", - "Algo como lo siguiente:\n", + "El esquema sería algo como lo siguiente:\n", "\n", " >>> d = pd.DataFrame(...)\n", " >>> np.sum(d) --> d.sum() --> np.nansum(d.values) --> d.values._sum()\n" @@ -699,7 +709,7 @@ "\n", " pure_numpy_stmt : pd.Series(np.nansum(d.values), index=d.columns)\n", "\n", - "El caso pure_numpy es lo que se diría _\"ir al grano\"_. En este caso sabemos lo que hacen las librerías y se lo ponemos fácil:\n", + "El caso pure_numpy es lo que se llamaría _\"ir al grano\"_. En este caso sabemos lo que hacen las librerías y se lo ponemos fácil:\n", " \n", " >>> d = pd.DataFrame\n", " >>> pd.Series(\n", @@ -716,47 +726,37 @@ "\n", " bottleneck_stmt : pd.Series(bn.nansum(d.values), index=d.columns)\n", "\n", - "Este paso, ahorra muchas llamadas intermedias si pandas efectivamente puede usar `bottleneck`.\n", + "Este paso ahorra muchas llamadas intermedias si `Pandas` puede usar `Bottleneck`.\n", "\n", " >>> d = pd.DataFrame\n", " >>> pd.Series(\n", " bn.nansum(d.values), # en bottleneck directamente\n", " index=d.columns\n", " )\n", - " " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ + " \n", + "\n", "> Nota: Dado que sabemos que no tenemos `NaNs` podríamos ir un paso más allá y aplicar la `np.sum`, pero estaríamos siendo injustos con `Pandas`.\n", "\n", - "> Nota 2: En el caso de la suma, pandas no usa bottleneck (más detalles en el siguiente apartado)." + "> Nota 2: En el caso de la suma, `Pandas` no usa `Bottleneck` (más detalles en el siguiente apartado)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# El papel de bottleneck en Pandas\n", + "# El papel de `Bottleneck` en `Pandas`\n", "\n", - "Como hemos comentado antes, `Pandas` usa para algunos cálculos `bottleneck` en lugar de `numpy`, esto es debido a que `bottleneck` tiene un conjunto de funciones que gestionan `NaNs` implementadas en C, lo cual le dota de una gran velocidad de cálculo. Las funciones que hacen uso de bottleneck, siempre y cuando sea posible, son las siguientes:\n", + "Como hemos comentado antes, `Pandas` usa para algunos cálculos `Bottleneck` en lugar de `NumPy`, esto es debido a que `bottleneck` tiene un conjunto de funciones que gestionan `NaNs` implementadas en C, lo cual le dota de una gran velocidad de cálculo. Las funciones que tratan hacer uso de `Bottleneck` son las siguientes:\n", "\n", - " - mean\n", - " - median\n", - " - std\n", - " - var\n", - " - min\n", - " - max\n", + " - `mean`\n", + " - `median`\n", + " - `std`\n", + " - `var`\n", + " - `min`\n", + " - `max`\n", + "\n", + "Esto lo podemos ver en `pandas.core.nanops`, todas la funciones que usan el decorador `@bottleneck_switch` intentarán usar `Bottleneck` para realizar la operación.\n", "\n", - "Esto lo podemos ver en `pandas.core.nanops`, todas la funciones que usan el decorador `@bottleneck_switch` intentarán usar `bottleneck` para realizar la operación." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ "Si tenéis curiosidad por este decorador os lo dejo por aquí:" ] }, @@ -808,17 +808,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Ahora ya sabéis qué hace Pandas y NumPy por debajo cuando se los llama como hemos visto. Pandas, a medida que aumenta el tamaño de los datos, se vuelve más eficiente, sin embargo, en ningún caso, gana a una implementación \"ad hoc\" como las pure_numpy o bottleneck. Siempre se puede atajar, pero Pandas lo hace muy bien en general, no nos tenemos que preocupar de si lo que devuelve es una Serie, DataFrame, array, int, float, etc, nos devuelve lo que tiene que ser y sin preocuparnos por los indices, columnas, nombres, dtypes... por lo que, no está nada mal. \n", + "Ahora ya sabéis qué hace `Pandas` y `NumPy` por debajo cuando se los llama como hemos visto. `Pandas`, a medida que aumenta el tamaño de los datos, se vuelve más eficiente, sin embargo, en ningún caso, gana a una implementación \"ad hoc\" como las *pure_numpy* o *bottleneck*. Siempre se puede atajar, pero `Pandas` lo hace muy bien en general, no nos tenemos que preocupar de si lo que devuelve es una `Serie`, `DataFrame`, `array`, `int`, `float`, etc, nos devuelve lo que tiene que ser, y por si fuera poco sin preocuparnos por los índices, columnas, nombres, dtypes... si lo miramos todo en conjunto no está nada mal.\n", "\n", "That's all folks!" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -837,7 +830,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.4" + "version": "3.6.5" } }, "nbformat": 4, From 54bf8190948b7904e9592cff1b23fbfc99989c13 Mon Sep 17 00:00:00 2001 From: Max Greco Date: Thu, 19 Apr 2018 11:38:51 +0200 Subject: [PATCH 3/3] CLN: Fix typos --- .../notebooks/performance_np_vs_pd.ipynb | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/content/downloads/notebooks/performance_np_vs_pd.ipynb b/content/downloads/notebooks/performance_np_vs_pd.ipynb index 59e81a8c..e603fa62 100644 --- a/content/downloads/notebooks/performance_np_vs_pd.ipynb +++ b/content/downloads/notebooks/performance_np_vs_pd.ipynb @@ -6,7 +6,7 @@ "source": [ "# Introducción\n", "\n", - "Hace varias de semanas salió un [proyecto muy interesante](https://github.com/mm-mansour/Fast-Pandas) en el que se compara la performance de `Pandas` con `NumPy`. Bien, dado que uso `Pandas` y `NumPy` a diario no me costó demasiado encontrar algunas cosas (quizá algo difusas) que estarían bien comentar o matizar. Sobre todo cuando vemos comparaciones de este estilo, ya que nos podemos quedar con alguna que otra idea errónea.\n", + "Hace varias semanas salió un [proyecto muy interesante](https://github.com/mm-mansour/Fast-Pandas) en el que se compara la performance de `Pandas` con `NumPy`. Bien, dado que uso `Pandas` y `NumPy` a diario no me costó demasiado encontrar algunas cosas (quizá algo difusas) que estarían bien comentar o matizar. Sobre todo cuando vemos comparaciones de este estilo, ya que nos podemos quedar con alguna que otra idea errónea.\n", "\n", "A continuación voy a tratar de explicar cómo funciona `Pandas` y `NumPy` cuando hacemos llamadas del estilo:\n", "\n", @@ -155,7 +155,7 @@ " >>> pure_numpy_stmt = \"pd.Series(np.nansum(d.values), index=d.columns)\"\n", " >>> bottleneck_stmt = \"pd.Series(bn.nansum(d.values), index=d.columns)\"\n", "\n", - "Estas llamadas se explican con más detalle más abajo, de momento vamos a quedarnos con que son algunas de las posibles alternativas para calcular la `sum` de un `DataFrame` y que nos __devuelen el mismo objeto__ en los 4 casos.\n", + "Estas llamadas se explican con más detalle más abajo, de momento vamos a quedarnos con que son algunas de las posibles alternativas para calcular la `sum` de un `DataFrame` y que nos __devuelve el mismo objeto__ en los 4 casos.\n", "\n", "Para realizar la comparación de tiempos he usado la función `timeit_results` definida arriba, esta función usa un mini proyecto para poder hacer este tipo de comparaciones de forma más cómoda.\n" ] @@ -199,7 +199,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Los statements (`stmt`) los he escrito de tal forma que devuelvan el mismo objeto, un `pandas.Series` en este caso dado que es lo que devuelve `d.sum()`. Así nos estamos asegurando de que el usuario de `pandas` tiene como resultado el mismo objeto y los tiempos son comparables entre sí." + "Los statements (`stmt`) los he escrito de tal forma que devuelvan el mismo objeto, un `pandas.Series` en este caso dado que es lo que devuelve `d.sum()`. De esta forma nos aseguramos de que el usuario de `pandas` tiene como resultado el mismo objeto y los tiempos son comparables entre sí." ] }, { @@ -514,7 +514,7 @@ "\n", "> Nota: Recordemos que `d` es un `pandas.DataFrame`. \n", "\n", - "El gráfico 1 muestra en el eje de las absisas los nombres de los statements y en el eje de las ordenadas los tiempos en segundos. Los gráficos están separados por tamaño del `DataFrame`, esto es debido a que hay mucha diferencia entre los tiempos en los extremos de N, y si los pintamos en un mismo gráfico no podríamos apreciar los detalles de cada caso.\n", + "El gráfico 1 muestra en el eje de las abscisas los nombres de los statements y en el eje de las ordenadas los tiempos en segundos. Los gráficos están separados por tamaño del `DataFrame`, esto es debido a que hay mucha diferencia entre los tiempos en los extremos de N, y si los pintamos en un mismo gráfico no podríamos apreciar los detalles de cada caso.\n", "\n", "Lo primero que resalta del gráfico 1 son los tiempos de los casos llamados _pandas_ y _numpy_, los cuales contrastan con los *pure_numpy* y _bottleneck_. Si vamos un poco más al detalle, _numpy_ (con el statement `np.nansum(d)`), es más lenta que la versión _pandas_, `d.sum()`, sin embargo, _numpy_ va alcanzando a _pandas_ a medida que aumenta N, hasta el punto en el que se invierten las posiciones, esto ocurre a partir del tamaño N=10^6, esto sólo es en media. Los tiempos varían mucho más en el caso _pandas_, y se aprecia en las barras de error que se aprecian claramente para los tamaños grandes.\n", "\n", @@ -748,12 +748,12 @@ "\n", "Como hemos comentado antes, `Pandas` usa para algunos cálculos `Bottleneck` en lugar de `NumPy`, esto es debido a que `bottleneck` tiene un conjunto de funciones que gestionan `NaNs` implementadas en C, lo cual le dota de una gran velocidad de cálculo. Las funciones que tratan hacer uso de `Bottleneck` son las siguientes:\n", "\n", - " - `mean`\n", - " - `median`\n", - " - `std`\n", - " - `var`\n", - " - `min`\n", - " - `max`\n", + "- `mean`\n", + "- `median`\n", + "- `std`\n", + "- `var`\n", + "- `min`\n", + "- `max`\n", "\n", "Esto lo podemos ver en `pandas.core.nanops`, todas la funciones que usan el decorador `@bottleneck_switch` intentarán usar `Bottleneck` para realizar la operación.\n", "\n", @@ -808,10 +808,17 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Ahora ya sabéis qué hace `Pandas` y `NumPy` por debajo cuando se los llama como hemos visto. `Pandas`, a medida que aumenta el tamaño de los datos, se vuelve más eficiente, sin embargo, en ningún caso, gana a una implementación \"ad hoc\" como las *pure_numpy* o *bottleneck*. Siempre se puede atajar, pero `Pandas` lo hace muy bien en general, no nos tenemos que preocupar de si lo que devuelve es una `Serie`, `DataFrame`, `array`, `int`, `float`, etc, nos devuelve lo que tiene que ser, y por si fuera poco sin preocuparnos por los índices, columnas, nombres, dtypes... si lo miramos todo en conjunto no está nada mal.\n", + "Ahora ya sabéis qué ocurre en `Pandas` y `NumPy` por debajo cuando se hacen las llamadas que hemos visto. `Pandas`, a medida que aumenta el tamaño de los datos, se vuelve más eficiente, sin embargo, en ningún caso, gana a una implementación \"ad hoc\" como las *pure_numpy* o *bottleneck*. Siempre se puede atajar, pero `Pandas` lo hace muy bien en general, no nos tenemos que preocupar de si lo que devuelve es una `Serie`, `DataFrame`, `array`, `int`, `float`, etc, nos devuelve lo que tiene que ser, y por si fuera poco sin preocuparnos por los índices, columnas, nombres, dtypes... si lo miramos todo en conjunto no está nada mal.\n", "\n", "That's all folks!" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -830,7 +837,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.5.4" } }, "nbformat": 4,