From 155fd4fc8acb22f7917401b1d822c8a23b0e2f73 Mon Sep 17 00:00:00 2001 From: Lu Weizheng Date: Mon, 25 Mar 2024 09:32:14 +0800 Subject: [PATCH] dask & mpi --- _toc.yml | 7 + ch-dask-dataframe/dask-expr.ipynb | 471 +++++ ch-dask-dataframe/dask-pandas.md | 3 + ch-dask-dataframe/index.md | 2 - ch-dask-dataframe/indexing.ipynb | 1043 ++++++++++ ch-dask-dataframe/map-partitions.ipynb | 391 ++++ ch-dask-dataframe/read-write.ipynb | 423 ++-- ch-dask-dataframe/shuffle-test.ipynb | 1712 +++++++++++++++++ ch-dask-dataframe/shuffle.ipynb | 59 + ch-dask/dask-distributed.ipynb | 6 +- ch-dask/task-graph-partitioning.ipynb | 2 +- ch-data-science/machine-learning.ipynb | 38 + ch-mpi-large-model/data-parallel.md | 61 +- ch-mpi-large-model/large-model.md | 9 + ch-mpi-large-model/nccl.md | 4 +- ch-mpi-large-model/neural-network-training.md | 7 + ch-mpi-large-model/pipeline-parallel.md | 60 +- ch-ray-data/data-transform.ipynb | 284 ++- .../ch-dask-dataframe/dataframe-model.drawio | 22 + .../model-training-input-output.drawio | 172 ++ drawio/ch-data-science/model-training.drawio | 298 +++ .../data-parallel-all-reduce.drawio | 330 ++++ .../data-parallel-distributed.drawio | 328 ++++ .../data-parallel-single.drawio | 163 ++ .../ch-mpi-large-model/data-parallel.drawio | 100 + .../pipeline-parallel-data-parallel.drawio | 294 +++ .../pipeline-parallel-distributed.drawio | 129 ++ .../pipeline-parallel.drawio | 100 + img/ch-dask-dataframe/dataframe-model.svg | 4 + img/ch-dask-dataframe/divisions.png | Bin 0 -> 236730 bytes img/ch-dask-dataframe/groupby.svg | 1616 ++++++++++++++++ img/ch-dask-dataframe/nyc-flights-graph.svg | 168 +- .../model-training-input-output.svg | 4 + img/ch-data-science/model-training.svg | 4 + .../data-parallel-all-reduce.svg | 4 + .../data-parallel-distributed.svg | 4 + .../data-parallel-single.svg | 4 + img/ch-mpi-large-model/data-parallel.svg | 4 + .../pipeline-parallel-data-parallel.svg | 4 + .../pipeline-parallel-distributed.svg | 4 + img/ch-mpi-large-model/pipeline-parallel.svg | 4 + references.bib | 44 +- 42 files changed, 8054 insertions(+), 332 deletions(-) create mode 100644 ch-dask-dataframe/dask-expr.ipynb create mode 100644 ch-dask-dataframe/dask-pandas.md create mode 100644 ch-dask-dataframe/indexing.ipynb create mode 100644 ch-dask-dataframe/map-partitions.ipynb create mode 100644 ch-dask-dataframe/shuffle-test.ipynb create mode 100644 ch-dask-dataframe/shuffle.ipynb create mode 100644 ch-mpi-large-model/large-model.md create mode 100644 ch-mpi-large-model/neural-network-training.md create mode 100644 drawio/ch-dask-dataframe/dataframe-model.drawio create mode 100644 drawio/ch-data-science/model-training-input-output.drawio create mode 100644 drawio/ch-data-science/model-training.drawio create mode 100644 drawio/ch-mpi-large-model/data-parallel-all-reduce.drawio create mode 100644 drawio/ch-mpi-large-model/data-parallel-distributed.drawio create mode 100644 drawio/ch-mpi-large-model/data-parallel-single.drawio create mode 100644 drawio/ch-mpi-large-model/data-parallel.drawio create mode 100644 drawio/ch-mpi-large-model/pipeline-parallel-data-parallel.drawio create mode 100644 drawio/ch-mpi-large-model/pipeline-parallel-distributed.drawio create mode 100644 drawio/ch-mpi-large-model/pipeline-parallel.drawio create mode 100644 img/ch-dask-dataframe/dataframe-model.svg create mode 100644 img/ch-dask-dataframe/divisions.png create mode 100644 img/ch-dask-dataframe/groupby.svg create mode 100644 img/ch-data-science/model-training-input-output.svg create mode 100644 img/ch-data-science/model-training.svg create mode 100644 img/ch-mpi-large-model/data-parallel-all-reduce.svg create mode 100644 img/ch-mpi-large-model/data-parallel-distributed.svg create mode 100644 img/ch-mpi-large-model/data-parallel-single.svg create mode 100644 img/ch-mpi-large-model/data-parallel.svg create mode 100644 img/ch-mpi-large-model/pipeline-parallel-data-parallel.svg create mode 100644 img/ch-mpi-large-model/pipeline-parallel-distributed.svg create mode 100644 img/ch-mpi-large-model/pipeline-parallel.svg diff --git a/_toc.yml b/_toc.yml index 8755167..d6fb845 100644 --- a/_toc.yml +++ b/_toc.yml @@ -22,7 +22,11 @@ subtrees: - file: ch-dask/task-graph-partitioning - file: ch-dask-dataframe/index entries: + - file: ch-dask-dataframe/dask-pandas - file: ch-dask-dataframe/read-write + - file: ch-dask-dataframe/indexing + - file: ch-dask-dataframe/map-partitions + - file: ch-dask-dataframe/shuffle - file: ch-ray-core/index entries: - file: ch-ray-core/ray-intro @@ -45,5 +49,8 @@ subtrees: - file: ch-mpi/remote-memory-access - file: ch-mpi-large-model/index entries: + - file: ch-mpi-large-model/large-model - file: ch-mpi-large-model/nccl + - file: ch-mpi-large-model/data-parallel + - file: ch-mpi-large-model/pipeline-parallel - file: ref diff --git a/ch-dask-dataframe/dask-expr.ipynb b/ch-dask-dataframe/dask-expr.ipynb new file mode 100644 index 0000000..45f7f70 --- /dev/null +++ b/ch-dask-dataframe/dask-expr.ipynb @@ -0,0 +1,471 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(dask-expr)=\n", + "# Dask Expressions\n", + "\n", + "在数据工程领域,比如 Spark 或者 SQL 数据库,有一些通用的、经典的优化方法,比如谓词下推(Predicate pushdown)。这些优化技术又被称为查询优化(Query Optimization),已经被数据工程深入研究过,它们可有效加速数据处理的速度。\n", + "\n", + "早期的 Dask DataFrame 并没有做这些优化。2023年开始,Dask DataFrame 推出了 Dask Expressions (dask-expr),专门用于查询优化,加速数据处理速度。Dask Expressions 保留了 Dask DataFrame 的 API,用户仍然使用原来的 API,只不过 Dask 在背后帮忙进行了查询优化。\n", + "\n", + "安装好后,可以直接 `import dask_expr as dd` 引入包,为了区别 `dask.dataframe` 也可以 `import dask_expr as dx`" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple\n", + "Requirement already satisfied: dask-expr in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (1.0.4)\n", + "Requirement already satisfied: dask==2024.3.1 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask-expr) (2024.3.1)\n", + "Requirement already satisfied: pyarrow>=7.0.0 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask-expr) (15.0.2)\n", + "Requirement already satisfied: pandas>=2 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask-expr) (2.2.1)\n", + "Requirement already satisfied: click>=8.1 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask==2024.3.1->dask-expr) (8.1.7)\n", + "Requirement already satisfied: cloudpickle>=1.5.0 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask==2024.3.1->dask-expr) (3.0.0)\n", + "Requirement already satisfied: fsspec>=2021.09.0 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask==2024.3.1->dask-expr) (2023.12.2)\n", + "Requirement already satisfied: packaging>=20.0 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask==2024.3.1->dask-expr) (23.2)\n", + "Requirement already satisfied: partd>=1.2.0 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask==2024.3.1->dask-expr) (1.4.1)\n", + "Requirement already satisfied: pyyaml>=5.3.1 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask==2024.3.1->dask-expr) (6.0.1)\n", + "Requirement already satisfied: toolz>=0.10.0 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask==2024.3.1->dask-expr) (0.12.0)\n", + "Requirement already satisfied: importlib-metadata>=4.13.0 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from dask==2024.3.1->dask-expr) (7.0.1)\n", + "Requirement already satisfied: numpy<2,>=1.23.2 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from pandas>=2->dask-expr) (1.26.3)\n", + "Requirement already satisfied: python-dateutil>=2.8.2 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from pandas>=2->dask-expr) (2.8.2)\n", + "Requirement already satisfied: pytz>=2020.1 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from pandas>=2->dask-expr) (2023.3.post1)\n", + "Requirement already satisfied: tzdata>=2022.7 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from pandas>=2->dask-expr) (2023.4)\n", + "Requirement already satisfied: zipp>=0.5 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from importlib-metadata>=4.13.0->dask==2024.3.1->dask-expr) (3.17.0)\n", + "Requirement already satisfied: locket in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from partd>=1.2.0->dask==2024.3.1->dask-expr) (1.0.0)\n", + "Requirement already satisfied: six>=1.5 in /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages (from python-dateutil>=2.8.2->pandas>=2->dask-expr) (1.16.0)\n" + ] + } + ], + "source": [ + "!pip install dask-expr" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import dask.dataframe as dd\n", + "import dask_expr as dx" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import shutil\n", + "import urllib.request\n", + "\n", + "folder_path = os.path.join(os.getcwd(), \"../data/nyc-taxi\")\n", + "download_url = [\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-01.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-02.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-03.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-04.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-05.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-06.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-07.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-08.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-09.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-10.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-11.parquet\",\n", + " \"https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-12.parquet\"\n", + "]\n", + "if not os.path.exists(folder_path):\n", + " os.makedirs(folder_path)\n", + "for url in download_url:\n", + " file_name = url.split(\"/\")[-1]\n", + " parquet_file_path = os.path.join(folder_path, file_name)\n", + " if not os.path.exists(os.path.join(folder_path, file_name)):\n", + " with urllib.request.urlopen(url) as response, open(parquet_file_path, 'wb') as out_file:\n", + " shutil.copyfileobj(response, out_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "先看一个没有经过优化的例子:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "284 ms ± 2.06 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "ddf = dd.read_parquet(os.path.join(folder_path, \"*.parquet\"))\n", + "payment_filtered = (\n", + " ddf[ddf.payment_type == 1]['tip_amount']\n", + ")\n", + "payment_filtered_mean = payment_filtered.mean().compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "首先将所有数据读取到内存中,然后过滤 `payment_type` 为 1 的行,并只需要 `tip_amount` 列。行和列的过滤本来可以在数据读取阶段提前进行。也就是查询优化中的谓词下推:在离数据读取越近的地方,提前进行数据的过滤,避免那些不会被用到的数据被读取进来。\n", + "\n", + "如果 Dask DataFrame 用户对查询优化比较熟悉,可以将上面的代码修改为:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "146 ms ± 1.26 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "ddf = dd.read_parquet(\n", + " os.path.join(folder_path, \"*.parquet\"),\n", + " filters=[(\"payment_type\", \"==\", 1)],\n", + " columns=[\"tip_amount\"],\n", + ")\n", + "payment_filtered_mean = ddf.tip_amount.mean().compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "幸运地是,Dask DataFrame 现在提供了一种自动的优化方式:Dask Expressions,它不需要用户了解查询优化。Dask Expressions " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sum: numeric_only=True\n", + " Projection: columns='tip_amount'\n", + " Filter:\n", + " ReadParquetFSSpec: path='/Users/luweizheng/Projects/godaai/distributed-python/ch-dask-dataframe/../data/nyc-taxi/*.parquet' kwargs={'dtype_backend': None}\n", + " EQ: right=1\n", + " Projection: columns='payment_type'\n", + " ReadParquetFSSpec: path='/Users/luweizheng/Projects/godaai/distributed-python/ch-dask-dataframe/../data/nyc-taxi/*.parquet' kwargs={'dtype_backend': None}\n" + ] + } + ], + "source": [ + "ddf = dx.read_parquet(os.path.join(folder_path, \"*.parquet\"))\n", + "payment_filtered = (\n", + " ddf[ddf.payment_type == 1]['tip_amount']\n", + ")\n", + "payment_filtered_mean = payment_filtered.sum(numeric_only=True)\n", + "payment_filtered_mean.pprint()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "148 ms ± 1.11 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "ddf = dx.read_parquet(\n", + " os.path.join(folder_path, \"*.parquet\"),\n", + " filters=[(\"payment_type\", \"==\", 1)],\n", + " columns=[\"tip_amount\"],\n", + ")\n", + "payment_filtered_mean = ddf.tip_amount.mean().optimize().compute()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'numpy.float64' object has no attribute 'compute'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32m:1\u001b[0m\n", + "\u001b[0;31mAttributeError\u001b[0m: 'numpy.float64' object has no attribute 'compute'" + ] + } + ], + "source": [ + "%%time\n", + "payment_filtered_mean.compute()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'numpy.float64' object has no attribute 'optimize'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32m:1\u001b[0m\n", + "\u001b[0;31mAttributeError\u001b[0m: 'numpy.float64' object has no attribute 'optimize'" + ] + } + ], + "source": [ + "%%time\n", + "optimized_payment_filtered_mean = payment_filtered_mean.optimize()\n", + "optimized_payment_filtered_mean.pprint()\n", + "optimized_payment_filtered_mean.compute()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'optimized_payment_filtered_mean' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[10], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43moptimized_payment_filtered_mean\u001b[49m\u001b[38;5;241m.\u001b[39msimplify()\u001b[38;5;241m.\u001b[39mpprint()\n", + "\u001b[0;31mNameError\u001b[0m: name 'optimized_payment_filtered_mean' is not defined" + ] + } + ], + "source": [ + "optimized_payment_filtered_mean.simplify().pprint()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "281 ms ± 3.24 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit\n", + "optimized_payment_filtered_mean.compute()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Dask DataFrame Structure:
\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", + "
nameidxy
npartitions=364
2024-01-01stringint64float64float64
2024-01-02............
...............
2024-12-29............
2024-12-30............
\n", + "
Dask Name: to_string_dtype, 2 expressions
" + ], + "text/plain": [ + "Dask DataFrame Structure:\n", + " name id x y\n", + "npartitions=364 \n", + "2024-01-01 string int64 float64 float64\n", + "2024-01-02 ... ... ... ...\n", + "... ... ... ... ...\n", + "2024-12-29 ... ... ... ...\n", + "2024-12-30 ... ... ... ...\n", + "Dask Name: to_string_dtype, 2 expressions\n", + "Expr=ArrowStringConversion(frame=Timeseries(5e5bcf2))" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = dx.datasets.timeseries(\n", + " start=\"2024-01-01\", \n", + " end=\"2024-12-30\", \n", + " freq=\"100ms\",\n", + ")\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "'ArrowStringArray' with dtype string does not support reduction 'sum' with pyarrow version 15.0.2. 'sum' may be supported by upgrading pyarrow.", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mArrowNotImplementedError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/arrays/arrow/array.py:1685\u001b[0m, in \u001b[0;36mArrowExtensionArray._reduce_pyarrow\u001b[0;34m(self, name, skipna, **kwargs)\u001b[0m\n\u001b[1;32m 1684\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 1685\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mpyarrow_meth\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdata_to_reduce\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mskip_nulls\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mskipna\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1686\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (\u001b[38;5;167;01mAttributeError\u001b[39;00m, \u001b[38;5;167;01mNotImplementedError\u001b[39;00m, \u001b[38;5;167;01mTypeError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m err:\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pyarrow/compute.py:263\u001b[0m, in \u001b[0;36m_make_generic_wrapper..wrapper\u001b[0;34m(memory_pool, options, *args, **kwargs)\u001b[0m\n\u001b[1;32m 262\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Expression\u001b[38;5;241m.\u001b[39m_call(func_name, \u001b[38;5;28mlist\u001b[39m(args), options)\n\u001b[0;32m--> 263\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcall\u001b[49m\u001b[43m(\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43moptions\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmemory_pool\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pyarrow/_compute.pyx:385\u001b[0m, in \u001b[0;36mpyarrow._compute.Function.call\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pyarrow/error.pxi:154\u001b[0m, in \u001b[0;36mpyarrow.lib.pyarrow_internal_check_status\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pyarrow/error.pxi:91\u001b[0m, in \u001b[0;36mpyarrow.lib.check_status\u001b[0;34m()\u001b[0m\n", + "\u001b[0;31mArrowNotImplementedError\u001b[0m: Function 'sum' has no kernel matching input types (large_string)", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[4], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m out \u001b[38;5;241m=\u001b[39m \u001b[43mdf\u001b[49m\u001b[43m[\u001b[49m\u001b[43mdf\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mid\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m==\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1000\u001b[39;49m\u001b[43m]\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msum\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mx\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_collection.py:1328\u001b[0m, in \u001b[0;36mFrameBase.sum\u001b[0;34m(self, axis, skipna, numeric_only, min_count, split_every, **kwargs)\u001b[0m\n\u001b[1;32m 1319\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m axis \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m 1320\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmap_partitions(\n\u001b[1;32m 1321\u001b[0m M\u001b[38;5;241m.\u001b[39msum,\n\u001b[1;32m 1322\u001b[0m skipna\u001b[38;5;241m=\u001b[39mskipna,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1325\u001b[0m min_count\u001b[38;5;241m=\u001b[39mmin_count,\n\u001b[1;32m 1326\u001b[0m )\n\u001b[0;32m-> 1328\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mnew_collection\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexpr\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msum\u001b[49m\u001b[43m(\u001b[49m\u001b[43mskipna\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnumeric_only\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msplit_every\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1329\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_apply_min_count(result, min_count)\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_collection.py:4578\u001b[0m, in \u001b[0;36mnew_collection\u001b[0;34m(expr)\u001b[0m\n\u001b[1;32m 4576\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mnew_collection\u001b[39m(expr):\n\u001b[1;32m 4577\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Create new collection from an expr\"\"\"\u001b[39;00m\n\u001b[0;32m-> 4578\u001b[0m meta \u001b[38;5;241m=\u001b[39m \u001b[43mexpr\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_meta\u001b[49m\n\u001b[1;32m 4579\u001b[0m expr\u001b[38;5;241m.\u001b[39m_name \u001b[38;5;66;03m# Ensure backend is imported\u001b[39;00m\n\u001b[1;32m 4580\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m get_collection_type(meta)(expr)\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/functools.py:1001\u001b[0m, in \u001b[0;36mcached_property.__get__\u001b[0;34m(self, instance, owner)\u001b[0m\n\u001b[1;32m 999\u001b[0m val \u001b[38;5;241m=\u001b[39m cache\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mattrname, _NOT_FOUND)\n\u001b[1;32m 1000\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m val \u001b[38;5;129;01mis\u001b[39;00m _NOT_FOUND:\n\u001b[0;32m-> 1001\u001b[0m val \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstance\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1002\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1003\u001b[0m cache[\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mattrname] \u001b[38;5;241m=\u001b[39m val\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_reductions.py:429\u001b[0m, in \u001b[0;36mApplyConcatApply._meta\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 427\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mcached_property\n\u001b[1;32m 428\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_meta\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[0;32m--> 429\u001b[0m meta \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_meta_chunk\u001b[49m\n\u001b[1;32m 430\u001b[0m aggregate \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39maggregate \u001b[38;5;129;01mor\u001b[39;00m (\u001b[38;5;28;01mlambda\u001b[39;00m x: x)\n\u001b[1;32m 431\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcombine:\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/functools.py:1001\u001b[0m, in \u001b[0;36mcached_property.__get__\u001b[0;34m(self, instance, owner)\u001b[0m\n\u001b[1;32m 999\u001b[0m val \u001b[38;5;241m=\u001b[39m cache\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mattrname, _NOT_FOUND)\n\u001b[1;32m 1000\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m val \u001b[38;5;129;01mis\u001b[39;00m _NOT_FOUND:\n\u001b[0;32m-> 1001\u001b[0m val \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstance\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1002\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1003\u001b[0m cache[\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mattrname] \u001b[38;5;241m=\u001b[39m val\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_reductions.py:425\u001b[0m, in \u001b[0;36mApplyConcatApply._meta_chunk\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 422\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mcached_property\n\u001b[1;32m 423\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_meta_chunk\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m 424\u001b[0m meta \u001b[38;5;241m=\u001b[39m meta_nonempty(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mframe\u001b[38;5;241m.\u001b[39m_meta)\n\u001b[0;32m--> 425\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mchunk\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmeta\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mchunk_kwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_reductions.py:740\u001b[0m, in \u001b[0;36mReduction.chunk\u001b[0;34m(cls, df, **kwargs)\u001b[0m\n\u001b[1;32m 738\u001b[0m \u001b[38;5;129m@classmethod\u001b[39m\n\u001b[1;32m 739\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mchunk\u001b[39m(\u001b[38;5;28mcls\u001b[39m, df, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m--> 740\u001b[0m out \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mcls\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mreduction_chunk\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdf\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 741\u001b[0m \u001b[38;5;66;03m# Return a dataframe so that the concatenated version is also a dataframe\u001b[39;00m\n\u001b[1;32m 742\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m out\u001b[38;5;241m.\u001b[39mto_frame()\u001b[38;5;241m.\u001b[39mT \u001b[38;5;28;01mif\u001b[39;00m is_series_like(out) \u001b[38;5;28;01melse\u001b[39;00m out\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/utils.py:1241\u001b[0m, in \u001b[0;36mmethodcaller.__call__\u001b[0;34m(self, _methodcaller__obj, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1240\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__call__\u001b[39m(\u001b[38;5;28mself\u001b[39m, __obj, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m-> 1241\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mgetattr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m__obj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmethod\u001b[49m\u001b[43m)\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/frame.py:11657\u001b[0m, in \u001b[0;36mDataFrame.sum\u001b[0;34m(self, axis, skipna, numeric_only, min_count, **kwargs)\u001b[0m\n\u001b[1;32m 11648\u001b[0m \u001b[38;5;129m@doc\u001b[39m(make_doc(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msum\u001b[39m\u001b[38;5;124m\"\u001b[39m, ndim\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m2\u001b[39m))\n\u001b[1;32m 11649\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21msum\u001b[39m(\n\u001b[1;32m 11650\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 11655\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[1;32m 11656\u001b[0m ):\n\u001b[0;32m> 11657\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msum\u001b[49m\u001b[43m(\u001b[49m\u001b[43maxis\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mskipna\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnumeric_only\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmin_count\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 11658\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\u001b[38;5;241m.\u001b[39m__finalize__(\u001b[38;5;28mself\u001b[39m, method\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msum\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/generic.py:12503\u001b[0m, in \u001b[0;36mNDFrame.sum\u001b[0;34m(self, axis, skipna, numeric_only, min_count, **kwargs)\u001b[0m\n\u001b[1;32m 12495\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21msum\u001b[39m(\n\u001b[1;32m 12496\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 12497\u001b[0m axis: Axis \u001b[38;5;241m|\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 12501\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[1;32m 12502\u001b[0m ):\n\u001b[0;32m> 12503\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_min_count_stat_function\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 12504\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43msum\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnanops\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mnansum\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mskipna\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnumeric_only\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmin_count\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[1;32m 12505\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/generic.py:12486\u001b[0m, in \u001b[0;36mNDFrame._min_count_stat_function\u001b[0;34m(self, name, func, axis, skipna, numeric_only, min_count, **kwargs)\u001b[0m\n\u001b[1;32m 12483\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m axis \u001b[38;5;129;01mis\u001b[39;00m lib\u001b[38;5;241m.\u001b[39mno_default:\n\u001b[1;32m 12484\u001b[0m axis \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0\u001b[39m\n\u001b[0;32m> 12486\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_reduce\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 12487\u001b[0m \u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 12488\u001b[0m \u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 12489\u001b[0m \u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43maxis\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 12490\u001b[0m \u001b[43m \u001b[49m\u001b[43mskipna\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mskipna\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 12491\u001b[0m \u001b[43m \u001b[49m\u001b[43mnumeric_only\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mnumeric_only\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 12492\u001b[0m \u001b[43m \u001b[49m\u001b[43mmin_count\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmin_count\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 12493\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/frame.py:11549\u001b[0m, in \u001b[0;36mDataFrame._reduce\u001b[0;34m(self, op, name, axis, skipna, numeric_only, filter_type, **kwds)\u001b[0m\n\u001b[1;32m 11545\u001b[0m df \u001b[38;5;241m=\u001b[39m df\u001b[38;5;241m.\u001b[39mT\n\u001b[1;32m 11547\u001b[0m \u001b[38;5;66;03m# After possibly _get_data and transposing, we are now in the\u001b[39;00m\n\u001b[1;32m 11548\u001b[0m \u001b[38;5;66;03m# simple case where we can use BlockManager.reduce\u001b[39;00m\n\u001b[0;32m> 11549\u001b[0m res \u001b[38;5;241m=\u001b[39m \u001b[43mdf\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_mgr\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mreduce\u001b[49m\u001b[43m(\u001b[49m\u001b[43mblk_func\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 11550\u001b[0m out \u001b[38;5;241m=\u001b[39m df\u001b[38;5;241m.\u001b[39m_constructor_from_mgr(res, axes\u001b[38;5;241m=\u001b[39mres\u001b[38;5;241m.\u001b[39maxes)\u001b[38;5;241m.\u001b[39miloc[\u001b[38;5;241m0\u001b[39m]\n\u001b[1;32m 11551\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m out_dtype \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m out\u001b[38;5;241m.\u001b[39mdtype \u001b[38;5;241m!=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mboolean\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/internals/managers.py:1500\u001b[0m, in \u001b[0;36mBlockManager.reduce\u001b[0;34m(self, func)\u001b[0m\n\u001b[1;32m 1498\u001b[0m res_blocks: \u001b[38;5;28mlist\u001b[39m[Block] \u001b[38;5;241m=\u001b[39m []\n\u001b[1;32m 1499\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m blk \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mblocks:\n\u001b[0;32m-> 1500\u001b[0m nbs \u001b[38;5;241m=\u001b[39m \u001b[43mblk\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mreduce\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1501\u001b[0m res_blocks\u001b[38;5;241m.\u001b[39mextend(nbs)\n\u001b[1;32m 1503\u001b[0m index \u001b[38;5;241m=\u001b[39m Index([\u001b[38;5;28;01mNone\u001b[39;00m]) \u001b[38;5;66;03m# placeholder\u001b[39;00m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/internals/blocks.py:404\u001b[0m, in \u001b[0;36mBlock.reduce\u001b[0;34m(self, func)\u001b[0m\n\u001b[1;32m 398\u001b[0m \u001b[38;5;129m@final\u001b[39m\n\u001b[1;32m 399\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mreduce\u001b[39m(\u001b[38;5;28mself\u001b[39m, func) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mlist\u001b[39m[Block]:\n\u001b[1;32m 400\u001b[0m \u001b[38;5;66;03m# We will apply the function and reshape the result into a single-row\u001b[39;00m\n\u001b[1;32m 401\u001b[0m \u001b[38;5;66;03m# Block with the same mgr_locs; squeezing will be done at a higher level\u001b[39;00m\n\u001b[1;32m 402\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mndim \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m2\u001b[39m\n\u001b[0;32m--> 404\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalues\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 406\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mvalues\u001b[38;5;241m.\u001b[39mndim \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m 407\u001b[0m res_values \u001b[38;5;241m=\u001b[39m result\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/frame.py:11457\u001b[0m, in \u001b[0;36mDataFrame._reduce..blk_func\u001b[0;34m(values, axis)\u001b[0m\n\u001b[1;32m 11455\u001b[0m dtype_has_keepdims[values\u001b[38;5;241m.\u001b[39mdtype] \u001b[38;5;241m=\u001b[39m has_keepdims\n\u001b[1;32m 11456\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m has_keepdims:\n\u001b[0;32m> 11457\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mvalues\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_reduce\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mskipna\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mskipna\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkeepdims\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 11458\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 11459\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\n\u001b[1;32m 11460\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtype\u001b[39m(values)\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m._reduce will require a `keepdims` parameter \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 11461\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124min the future\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 11462\u001b[0m \u001b[38;5;167;01mFutureWarning\u001b[39;00m,\n\u001b[1;32m 11463\u001b[0m stacklevel\u001b[38;5;241m=\u001b[39mfind_stack_level(),\n\u001b[1;32m 11464\u001b[0m )\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/arrays/string_arrow.py:567\u001b[0m, in \u001b[0;36mArrowStringArray._reduce\u001b[0;34m(self, name, skipna, keepdims, **kwargs)\u001b[0m\n\u001b[1;32m 564\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_reduce\u001b[39m(\n\u001b[1;32m 565\u001b[0m \u001b[38;5;28mself\u001b[39m, name: \u001b[38;5;28mstr\u001b[39m, \u001b[38;5;241m*\u001b[39m, skipna: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m, keepdims: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 566\u001b[0m ):\n\u001b[0;32m--> 567\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_reduce_calc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mskipna\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mskipna\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkeepdims\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mkeepdims\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 568\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m name \u001b[38;5;129;01min\u001b[39;00m (\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124margmin\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124margmax\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(result, pa\u001b[38;5;241m.\u001b[39mArray):\n\u001b[1;32m 569\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_convert_int_dtype(result)\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/arrays/arrow/array.py:1761\u001b[0m, in \u001b[0;36mArrowExtensionArray._reduce_calc\u001b[0;34m(self, name, skipna, keepdims, **kwargs)\u001b[0m\n\u001b[1;32m 1758\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_reduce_calc\u001b[39m(\n\u001b[1;32m 1759\u001b[0m \u001b[38;5;28mself\u001b[39m, name: \u001b[38;5;28mstr\u001b[39m, \u001b[38;5;241m*\u001b[39m, skipna: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m, keepdims: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 1760\u001b[0m ):\n\u001b[0;32m-> 1761\u001b[0m pa_result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_reduce_pyarrow\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mskipna\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mskipna\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1763\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m keepdims:\n\u001b[1;32m 1764\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(pa_result, pa\u001b[38;5;241m.\u001b[39mScalar):\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/arrays/arrow/array.py:1693\u001b[0m, in \u001b[0;36mArrowExtensionArray._reduce_pyarrow\u001b[0;34m(self, name, skipna, **kwargs)\u001b[0m\n\u001b[1;32m 1686\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (\u001b[38;5;167;01mAttributeError\u001b[39;00m, \u001b[38;5;167;01mNotImplementedError\u001b[39;00m, \u001b[38;5;167;01mTypeError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m err:\n\u001b[1;32m 1687\u001b[0m msg \u001b[38;5;241m=\u001b[39m (\n\u001b[1;32m 1688\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtype\u001b[39m(\u001b[38;5;28mself\u001b[39m)\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m with dtype \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdtype\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 1689\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mdoes not support reduction \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m with pyarrow \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 1690\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mversion \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mpa\u001b[38;5;241m.\u001b[39m__version__\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m. \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m may be supported by \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 1691\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mupgrading pyarrow.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 1692\u001b[0m )\n\u001b[0;32m-> 1693\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(msg) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01merr\u001b[39;00m\n\u001b[1;32m 1694\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m name \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmedian\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 1695\u001b[0m \u001b[38;5;66;03m# GH 52679: Use quantile instead of approximate_median; returns array\u001b[39;00m\n\u001b[1;32m 1696\u001b[0m result \u001b[38;5;241m=\u001b[39m result[\u001b[38;5;241m0\u001b[39m]\n", + "\u001b[0;31mTypeError\u001b[0m: 'ArrowStringArray' with dtype string does not support reduction 'sum' with pyarrow version 15.0.2. 'sum' may be supported by upgrading pyarrow." + ] + } + ], + "source": [ + "out = df[df.id == 1000].sum()[\"x\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dispy", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ch-dask-dataframe/dask-pandas.md b/ch-dask-dataframe/dask-pandas.md new file mode 100644 index 0000000..e83c41e --- /dev/null +++ b/ch-dask-dataframe/dask-pandas.md @@ -0,0 +1,3 @@ +# Dask DataFrame 与 pandas + +pandas 已经成为 DataFrame 的标准,但无法利用多核和集群,Dask DataFrame 试图解决 pandas 并行计算的问题。Dask DataFrame 尽量提供与 pandas 一致的 API,但使用起来,Dask DataFrame 仍有很多不同。Dask DataFrame 将大数据切分成小的 Partition,每个 Partition 是一个 pandas DataFrame。Dask 会把 DataFrame 的元数据记录下来,存储在 `_meta` 中。多个 Partition 的信息存储在 `partitions` 里和 `divisions` 里。Dask 用 Task Graph 管理计算图。对于用户来说,其实不需要太深入理解 Dask DataFrame 具体如何实现的,只需要调用类 pandas 的上层 API。本章假设用户已经了解并熟悉 pandas,并重点讨论 Dask DataFrame 与 pandas 的区别。 \ No newline at end of file diff --git a/ch-dask-dataframe/index.md b/ch-dask-dataframe/index.md index 11ead8b..5a59379 100644 --- a/ch-dask-dataframe/index.md +++ b/ch-dask-dataframe/index.md @@ -1,6 +1,4 @@ # Dask DataFrame -pandas 已经成为 DataFrame 的标准,但无法利用多核和集群,Dask DataFrame 试图解决 pandas 并行计算的问题。Dask DataFrame 尽量提供与 pandas 一致的 API,但使用起来,Dask DataFrame 仍有很多不同。本章假设用户已经了解并熟悉 pandas,并重点讨论 Dask DataFrame 与 pandas 的区别。 - ```{tableofcontents} ``` \ No newline at end of file diff --git a/ch-dask-dataframe/indexing.ipynb b/ch-dask-dataframe/indexing.ipynb new file mode 100644 index 0000000..e90f3e6 --- /dev/null +++ b/ch-dask-dataframe/indexing.ipynb @@ -0,0 +1,1043 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(dask-dataframe-indexing)=\n", + "# 索引" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [ + "hide-cell" + ] + }, + "outputs": [], + "source": [ + "%config InlineBackend.figure_format = 'svg'\n", + "import os\n", + "import urllib\n", + "import shutil\n", + "from zipfile import ZipFile\n", + "\n", + "import dask\n", + "import dask.dataframe as dd\n", + "import pandas as pd\n", + "from dask.distributed import LocalCluster, Client\n", + "\n", + "cluster = LocalCluster()\n", + "client = Client(cluster)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "如 {numref}`pandas-dataframe-model` 所示,pandas DataFrame 主要对二维的表进行处理,有列标签和行标签。行标签通常会被用户忽视,但实际上起着至关重要的作用,比如索引(Indexing)。大多数 pandas DataFrame 的行标签是排好序的索引,比如从 0 开始递增。 DataFrame 里面的数据也是有序的。\n", + "\n", + "```{figure} ../img/ch-dask-dataframe/dataframe-model.svg\n", + "---\n", + "width: 200px\n", + "name: pandas-dataframe-model\n", + "---\n", + "pandas DataFrame 数据模型\n", + "```\n", + "\n", + "创建 pandas DataFrame 时,会在最左侧自动生成了索引列,它不是 DataFrame 的“官方”字段,因为索引列并没有列名。" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "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", + "
ABCD
0fooone110
1barone220
2baztwo330
3quxthree440
\n", + "
" + ], + "text/plain": [ + " A B C D\n", + "0 foo one 1 10\n", + "1 bar one 2 20\n", + "2 baz two 3 30\n", + "3 qux three 4 40" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.DataFrame({\n", + " 'A': ['foo', 'bar', 'baz', 'qux'],\n", + " 'B': ['one', 'one', 'two', 'three'],\n", + " 'C': [1, 2, 3, 4],\n", + " 'D': [10, 20, 30, 40]\n", + "})\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "也可以设置一个字段作为索引列:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "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", + "
BCD
A
fooone110
barone220
baztwo330
quxthree440
\n", + "
" + ], + "text/plain": [ + " B C D\n", + "A \n", + "foo one 1 10\n", + "bar one 2 20\n", + "baz two 3 30\n", + "qux three 4 40" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = df.set_index('A')\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "或者重置回原来的结构:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "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", + "
ABCD
0fooone110
1barone220
2baztwo330
3quxthree440
\n", + "
" + ], + "text/plain": [ + " A B C D\n", + "0 foo one 1 10\n", + "1 bar one 2 20\n", + "2 baz two 3 30\n", + "3 qux three 4 40" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = df.reset_index()\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 有序行索引\n", + "\n", + "Dask DataFrame 由多个 pandas DataFrame 组成,但如何在全局维度维护整个 Dask DataFrame 行标签和行顺序是一个很大的挑战。Dask DataFrame 并没有刻意保留全局有序性,也使得它无法支持所有 pandas DataFrame 的功能。\n", + "\n", + "如 {numref}`dask-dataframe-divisions` 所示,Dask DataFrame 在切分时有 `divisions`。 \n", + "\n", + "```{figure} ../img/ch-dask-dataframe/divisions.png\n", + "---\n", + "width: 400px\n", + "name: dask-dataframe-divisions\n", + "---\n", + "Dask DataFrame 的 `divisions`\n", + "```\n", + "\n", + "以 Dask 提供的样例数据函数 `dask.datasets.timeseries` 为例,它生成了时间序列,使用时间戳作为行标签,每个 Partition 的边界都被记录下来,存储在 `.divisions` 里。`len(divisons)` 等于 `npartitions + 1`。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "df.npartitions: 1826\n", + "df.divisions: 1827\n" + ] + } + ], + "source": [ + "ts_df = dask.datasets.timeseries(\"2018-01-01\", \"2023-01-01\")\n", + "print(f\"df.npartitions: {ts_df.npartitions}\")\n", + "print(f\"df.divisions: {len(ts_df.divisions)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dask DataFrame 没有记录每个 Partition 中有多少行,因此无法在全局角度支持基于行索引的操作,比如 `iloc`。" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NotImplementedError, 'DataFrame.iloc' only supports selecting columns. It must be used like 'df.iloc[:, column_indexer]'.\n" + ] + } + ], + "source": [ + "try:\n", + " ts_df.iloc[3].compute()\n", + "except Exception as e:\n", + " print(f\"{type(e).__name__}, {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "但是可以支持列标签,或者 `:` 这样的通配符:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "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", + "
idx
timestamp
2018-01-01 00:00:009840.660595
2018-01-01 00:00:01960-0.747564
2018-01-01 00:00:0210390.777117
2018-01-01 00:00:031038-0.501949
2018-01-01 00:00:049920.767979
.........
2022-12-31 23:59:551005-0.102774
2022-12-31 23:59:561040-0.648857
2022-12-31 23:59:571019-0.310174
2022-12-31 23:59:589870.889037
2022-12-31 23:59:59977-0.078216
\n", + "

157766400 rows × 2 columns

\n", + "
" + ], + "text/plain": [ + " id x\n", + "timestamp \n", + "2018-01-01 00:00:00 984 0.660595\n", + "2018-01-01 00:00:01 960 -0.747564\n", + "2018-01-01 00:00:02 1039 0.777117\n", + "2018-01-01 00:00:03 1038 -0.501949\n", + "2018-01-01 00:00:04 992 0.767979\n", + "... ... ...\n", + "2022-12-31 23:59:55 1005 -0.102774\n", + "2022-12-31 23:59:56 1040 -0.648857\n", + "2022-12-31 23:59:57 1019 -0.310174\n", + "2022-12-31 23:59:58 987 0.889037\n", + "2022-12-31 23:59:59 977 -0.078216\n", + "\n", + "[157766400 rows x 2 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ts_df.iloc[:, [1, 2]].compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "对于 CSV 文件,Dask DataFrame 并没有自动生成 `divisions`。" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(None, None, None, None, None, None, None)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "folder_path = os.path.join(os.getcwd(), \"../data/\")\n", + "download_url = \"https://dp.godaai.org/nyc-flights.zip\"\n", + "zip_file_path = os.path.join(folder_path, \"nyc-flights.zip\")\n", + "if not os.path.exists(os.path.join(folder_path, \"nyc-flights\")):\n", + " with urllib.request.urlopen(download_url) as response, open(zip_file_path, 'wb') as out_file:\n", + " shutil.copyfileobj(response, out_file)\n", + " zf = ZipFile(zip_file_path, 'r')\n", + " zf.extractall(folder_path)\n", + " zf.close()\n", + "file_path = os.path.join(folder_path, \"nyc-flights\", \"*.csv\")\n", + "flights_ddf = dd.read_csv(file_path,\n", + " parse_dates={'Date': [0, 1, 2]},\n", + " dtype={'TailNum': object,\n", + " 'CRSElapsedTime': float,\n", + " 'Cancelled': bool})\n", + "flights_ddf.divisions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "因为没有记录每个 Partition 有多少条数据,Dask DataFrame 无法很好地支持一些操作,比如 `median()` 这样的百分位操作,因为这些操作需要:(1) 对数据排序;(2) 定位到特定的行。" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NotImplementedError, Dask doesn't implement an exact median in all cases as this is hard to do in parallel. See the `median_approximate` method instead, which uses an approximate algorithm.\n" + ] + } + ], + "source": [ + "try:\n", + " flights_ddf['DepDelay'].median()\n", + "except Exception as e:\n", + " print(f\"{type(e).__name__}, {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 设置索引列\n", + "\n", + "### `set_index()`\n", + "\n", + "在 Dask DataFrame 中,我们可以使用 `set_index()` 方法手动设置某一列为索引列,这个操作除了设置某个字段为索引列,还会根据这个字段对全局数据进行排序,它打乱了原来每个 Partition 的数据排序,因此会有很高的成本。\n", + "\n", + "下面的例子展示了 `set_index()` 带来的变化:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " col1 col2\n", + "0 01 a\n", + "1 05 b\n", + "2 02 c\n", + " col1 col2\n", + "3 03 d\n", + "4 04 e\n" + ] + } + ], + "source": [ + "def print_partitions(ddf):\n", + " for i in range(ddf.npartitions):\n", + " print(ddf.partitions[i].compute())\n", + "\n", + "df = pd.DataFrame(\n", + " {\"col1\": [\"01\", \"05\", \"02\", \"03\", \"04\"], \"col2\": [\"a\", \"b\", \"c\", \"d\", \"e\"]}\n", + ")\n", + "ddf = dd.from_pandas(df, npartitions=2)\n", + "print_partitions(ddf)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " col2\n", + "col1 \n", + "01 a\n", + " col2\n", + "col1 \n", + "02 c\n", + "03 d\n", + "04 e\n", + "05 b\n" + ] + } + ], + "source": [ + "ddf2 = ddf.set_index(\"col1\")\n", + "print_partitions(ddf2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "这个例子设置 `col1` 列为索引列,2 个 Partition 中的数据被打乱重排。如果是在数据量很大的场景,全局数据排序和重分布的成本极高。因此应该尽量避免这个操作。`set_index()` 也有它的优势,它可以加速下游的计算。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "回到时间序列数据,该数据使用时间戳作为索引列。下面使用了两种方式对这份数据 `set_index()`。第一种没有设置 `divisions`,第二种设置了 `divisions`。\n", + "\n", + "第一种不设置 `divisions` 耗时很长,因为 Dask DataFrame 计算了所有 Partiton 的数据分布,并根据分布重排列了所有的 Partition,可以看到,Partition 的数目也发生了变化。" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "before set_index npartitions: 1826\n", + "after set_index npartitions: 163\n", + "CPU times: user 6.1 s, sys: 3.47 s, total: 9.57 s\n", + "Wall time: 19.6 s\n" + ] + } + ], + "source": [ + "%%time\n", + "ts_df1 = ts_df.set_index(\"id\")\n", + "nu = ts_df1.loc[[1001]].name.nunique().compute()\n", + "print(f\"before set_index npartitions: {ts_df.npartitions}\")\n", + "print(f\"after set_index npartitions: {ts_df1.npartitions}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "第二种方式先提前获取了 `divisions`,然后将这些 `divisions` 用于设置 `set_index()`。设定 `division` 的 `set_index()` 速度更快。" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "dask_computed_divisions = ts_df.set_index(\"id\").divisions\n", + "unique_divisions = list(dict.fromkeys(list(dask_computed_divisions)))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.25 s, sys: 1.09 s, total: 4.34 s\n", + "Wall time: 11.7 s\n" + ] + } + ], + "source": [ + "%%time\n", + "ts_df2 = ts_df.set_index(\"id\", divisions=unique_divisions)\n", + "nuids = ts_df2.loc[[1001]].name.nunique().compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "如果不设置索引列,直接对 `id` 列进行查询,发现反而更快。" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.94 s, sys: 743 ms, total: 2.68 s\n", + "Wall time: 8.18 s\n" + ] + } + ], + "source": [ + "%%time\n", + "nu = ts_df.loc[ts_df[\"id\"] == 1001].name.nunique().compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "所以 Dask DataFrame 要慎重使用 `set_index()`,如果 `set_index()` 之后有很多以下操作,可以考虑使用 `set_index()`。\n", + "\n", + "* 使用 `loc` 对索引列进行过滤\n", + "* 两个 Dask DataFrame 在索引列上合并(`merge()`)\n", + "* 在索引列上进行分组聚合(`groupby()`)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### `reset_index()`\n", + "\n", + "在 pandas 中,默认 `as_index=True` 时,分组字段经过 `groupby()` 之后成为索引列。索引列在 DataFrame 中并不是正式的数据列,如果分组聚合之后只有一个字段(不考虑分组字段),分组聚合的结果就成了一个 `Series`。比如下面 pandas 的例子,`Origin` 列就是分组字段,如果不设置 `as_index=False`,`groupby(\"Origin\", as_index=False)[\"DepDelay\"].mean()` 生成的是一个 `Series`。" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "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", + "
OriginAvgDepDelay
2LGA5.726304
0EWR6.916220
1JFK9.311532
\n", + "
" + ], + "text/plain": [ + " Origin AvgDepDelay\n", + "2 LGA 5.726304\n", + "0 EWR 6.916220\n", + "1 JFK 9.311532" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# pandas\n", + "file_path = os.path.join(folder_path, \"nyc-flights\", \"1991.csv\")\n", + "pdf = pd.read_csv(file_path,\n", + " parse_dates={'Date': [0, 1, 2]},\n", + " dtype={'TailNum': object,\n", + " 'CRSElapsedTime': float,\n", + " 'Cancelled': bool})\n", + "uncancelled_pdf = pdf[pdf[\"Cancelled\"] == False]\n", + "avg_pdf = uncancelled_pdf.groupby(\"Origin\", as_index=False)[\"DepDelay\"].mean()\n", + "avg_pdf.columns = [\"Origin\", \"AvgDepDelay\"]\n", + "avg_pdf.sort_values(\"AvgDepDelay\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "或者是 `reset_index()`,来取消索引列,分组字段会成为 `DataFrame` 的一个正式的字段。" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "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", + "
OriginAvgDepDelay
2LGA5.726304
0EWR6.916220
1JFK9.311532
\n", + "
" + ], + "text/plain": [ + " Origin AvgDepDelay\n", + "2 LGA 5.726304\n", + "0 EWR 6.916220\n", + "1 JFK 9.311532" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "avg_pdf = uncancelled_pdf.groupby(\"Origin\")[\"DepDelay\"].mean().reset_index()\n", + "avg_pdf.columns = [\"Origin\", \"AvgDepDelay\"]\n", + "avg_pdf.sort_values(\"AvgDepDelay\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dask DataFrame 的 `groupby()` 不支持 `as_index` 参数。Dask DataFrame 只能使用 `reset_index()` 来取消索引列。" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "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", + "
OriginAvgDepDelay
2LGA6.944939
0EWR9.997188
1JFK10.766914
\n", + "
" + ], + "text/plain": [ + " Origin AvgDepDelay\n", + "2 LGA 6.944939\n", + "0 EWR 9.997188\n", + "1 JFK 10.766914" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "uncancelled_ddf = flights_ddf[flights_ddf[\"Cancelled\"] == False]\n", + "avg_ddf = uncancelled_ddf.groupby(\"Origin\")[\"DepDelay\"].mean().reset_index()\n", + "avg_ddf.columns = [\"Origin\", \"AvgDepDelay\"]\n", + "avg_ddf = avg_ddf.compute()\n", + "# pandas 只使用了一年数据,因此结果不一样\n", + "avg_ddf.sort_values(\"AvgDepDelay\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client.shutdown()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dispy", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ch-dask-dataframe/map-partitions.ipynb b/ch-dask-dataframe/map-partitions.ipynb new file mode 100644 index 0000000..4077e66 --- /dev/null +++ b/ch-dask-dataframe/map-partitions.ipynb @@ -0,0 +1,391 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# `map_partitions`\n", + "\n", + "除了 {numref}`dask-dataframe-shuffle` 中提到的一些需要通信的计算外,有一种最简单的并行方式,英文术语为 Embarrassingly Parallel,中文可翻译为易并行。它指的是该类计算不需要太多跨 Worker 的协调和通信。比如,对某个字段加一,每个 Worker 内执行加法操作即可,Worker 之间没有通信的开销。Dask DataFrame 中可以使用 `map_partitions()` 来做这类 Embarrassingly Parallel 的操作。`map_partitions(func)` 的参数是一个 `func`,这个 `func` 将在每个 Partition 上执行。\n", + "\n", + "下面的案例对缺失值进行填充,它没有跨 Worker 的通信开销,因此是一种 Embarrassingly Parallel 的典型应用场景。" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import urllib\n", + "import shutil\n", + "from zipfile import ZipFile\n", + "import warnings\n", + "\n", + "warnings.simplefilter(action='ignore', category=FutureWarning)\n", + "\n", + "folder_path = os.path.join(os.getcwd(), \"../data/\")\n", + "download_url_prefix = \"https://gender-pay-gap.service.gov.uk/viewing/download-data/\"\n", + "file_path_prefix = os.path.join(folder_path, \"gender-pay\")\n", + "if not os.path.exists(file_path_prefix):\n", + " os.makedirs(file_path_prefix)\n", + "for year in [2017, 2018, 2019, 2020, 2021, 2022]:\n", + " download_url = download_url_prefix + str(year)\n", + " file_path = os.path.join(file_path_prefix, f\"{str(year)}.csv\")\n", + " if not os.path.exists(file_path):\n", + " with urllib.request.urlopen(download_url) as response, open(file_path, 'wb') as out_file:\n", + " shutil.copyfileobj(response, out_file)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/distributed/node.py:182: UserWarning: Port 8787 is already in use.\n", + "Perhaps you already have a cluster running?\n", + "Hosting the HTTP server on port 57481 instead\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "import dask.dataframe as dd\n", + "import pandas as pd\n", + "from dask.distributed import LocalCluster, Client\n", + "\n", + "cluster = LocalCluster()\n", + "client = Client(cluster)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "ddf = dd.read_csv(os.path.join(file_path_prefix, \"*.csv\"),\n", + " dtype={'CompanyNumber': 'str', 'DiffMeanHourlyPercent': 'float64'})\n", + "\n", + "def fillna(df):\n", + " return df.fillna(value={\"PostCode\": \"UNKNOWN\"})\n", + " \n", + "ddf = ddf.map_partitions(fillna)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dask DataFrame 模拟了 pandas DataFrame,如果这个 API 的计算模式是 Embarrassingly Parallel,它的底层很可能就是使用 `map_partitions()` 实现的。\n", + "\n", + "{numref}`dask-dataframe-indexing` 提到过,Dask DataFrame 会在某个列上进行切分。我们可以在 `map_partitions()` 的 `func` 中实现任何我们想做的事情,但如果对这些切分的列做了改动,需要 `clear_divisions()` 或者重新 `set_index()`。" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Dask DataFrame Structure:
\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", + " \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", + " \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", + "
EmployerNameEmployerIdAddressPostCodeCompanyNumberSicCodesDiffMeanHourlyPercentDiffMedianHourlyPercentDiffMeanBonusPercentDiffMedianBonusPercentMaleBonusPercentFemaleBonusPercentMaleLowerQuartileFemaleLowerQuartileMaleLowerMiddleQuartileFemaleLowerMiddleQuartileMaleUpperMiddleQuartileFemaleUpperMiddleQuartileMaleTopQuartileFemaleTopQuartileCompanyLinkToGPGInfoResponsiblePersonEmployerSizeCurrentNameSubmittedAfterTheDeadlineDueDateDateSubmitted
npartitions=6
stringint64stringstringstringstringfloat64float64float64float64float64float64float64float64float64float64float64float64float64float64stringstringstringstringboolstringstring
.................................................................................
....................................................................................
.................................................................................
.................................................................................
\n", + "
\n", + "
Dask Name: fillna, 3 graph layers
" + ], + "text/plain": [ + "Dask DataFrame Structure:\n", + " EmployerName EmployerId Address PostCode CompanyNumber SicCodes DiffMeanHourlyPercent DiffMedianHourlyPercent DiffMeanBonusPercent DiffMedianBonusPercent MaleBonusPercent FemaleBonusPercent MaleLowerQuartile FemaleLowerQuartile MaleLowerMiddleQuartile FemaleLowerMiddleQuartile MaleUpperMiddleQuartile FemaleUpperMiddleQuartile MaleTopQuartile FemaleTopQuartile CompanyLinkToGPGInfo ResponsiblePerson EmployerSize CurrentName SubmittedAfterTheDeadline DueDate DateSubmitted\n", + "npartitions=6 \n", + " string int64 string string string string float64 float64 float64 float64 float64 float64 float64 float64 float64 float64 float64 float64 float64 float64 string string string string bool string string\n", + " ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...\n", + "... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...\n", + " ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...\n", + " ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...\n", + "Dask Name: fillna, 3 graph layers" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf.clear_divisions()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "client.shutdown()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dispy", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ch-dask-dataframe/read-write.ipynb b/ch-dask-dataframe/read-write.ipynb index c11d3a4..964ff11 100644 --- a/ch-dask-dataframe/read-write.ipynb +++ b/ch-dask-dataframe/read-write.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "(dask-read-write)=\n", + "(dask-dataframe-read-write)=\n", "# 读写数据\n", "\n", "Dask DataFrame 支持 pandas 中几乎所有的数据读写操作,包括从本地、NFS、HDFS 或 S3 上读写文本文件、Parquet、HDF、JSON 等格式的文件。 {numref}`dask-read-write-operations` 是几个常见的读写操作。\n", @@ -45,7 +45,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -62,13 +62,9 @@ "import urllib\n", "import shutil\n", "from zipfile import ZipFile\n", + "import warnings\n", "\n", - "import dask.dataframe as dd\n", - "import pandas as pd\n", - "from dask.distributed import LocalCluster, Client\n", - "\n", - "cluster = LocalCluster()\n", - "client = Client(cluster)\n", + "warnings.simplefilter(action='ignore', category=FutureWarning)\n", "\n", "folder_path = os.path.join(os.getcwd(), \"../data/\")\n", "download_url = \"https://dp.godaai.org/nyc-flights.zip\"\n", @@ -84,6 +80,31 @@ "print(file_path)" ] }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/distributed/node.py:182: UserWarning: Port 8787 is already in use.\n", + "Perhaps you already have a cluster running?\n", + "Hosting the HTTP server on port 55077 instead\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "import dask.dataframe as dd\n", + "import pandas as pd\n", + "from dask.distributed import LocalCluster, Client\n", + "\n", + "cluster = LocalCluster()\n", + "client = Client(cluster)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -93,7 +114,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -109,7 +130,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -132,7 +153,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -281,7 +302,7 @@ "[3 rows x 21 columns]" ] }, - "execution_count": 17, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -292,7 +313,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -436,7 +457,7 @@ "[3 rows x 21 columns]" ] }, - "execution_count": 18, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -456,7 +477,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -465,255 +486,255 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", - "-4992946418407614279\n", + "-7150382809904974857\n", "\n", "read-csv\n", "\n", - "\n", + "\n", "\n", - "-2791454735931807732\n", + "-6194689680917011400\n", "\n", "0\n", "\n", - "\n", + "\n", "\n", - "-4992946418407614279->-2791454735931807732\n", + "-7150382809904974857->-6194689680917011400\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "-1856934773411932769\n", + "6071941830101125281\n", "\n", "to_pyarrow_string\n", "\n", - "\n", + "\n", "\n", - "-2791454735931807732->-1856934773411932769\n", + "-6194689680917011400->6071941830101125281\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "-66498125748364619\n", + "-65660599333282282\n", "\n", "read-csv\n", "\n", - "\n", + "\n", "\n", - "3716186020170190693\n", + "312951075184987025\n", "\n", "1\n", "\n", - "\n", + "\n", "\n", - "-66498125748364619->3716186020170190693\n", + "-65660599333282282->312951075184987025\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "5227787437159759806\n", + "-8135483071169533727\n", "\n", "to_pyarrow_string\n", "\n", - "\n", + "\n", "\n", - "3716186020170190693->5227787437159759806\n", + "312951075184987025->-8135483071169533727\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "4172821046690527989\n", + "-1102500011605602925\n", "\n", "read-csv\n", "\n", - "\n", + "\n", "\n", - "-1176888008802505673\n", + "-8978480336772077469\n", "\n", "2\n", "\n", - "\n", + "\n", "\n", - "4172821046690527989->-1176888008802505673\n", + "-1102500011605602925->-8978480336772077469\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "5068749226614284286\n", + "-1050760860597841152\n", "\n", "to_pyarrow_string\n", "\n", - "\n", + "\n", "\n", - "-1176888008802505673->5068749226614284286\n", + "-8978480336772077469->-1050760860597841152\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "-7189200816447331052\n", + "-1906626916754175967\n", "\n", "read-csv\n", "\n", - "\n", + "\n", "\n", - "5330752747299492752\n", + "-2470839580670079044\n", "\n", "3\n", "\n", - "\n", + "\n", "\n", - "-7189200816447331052->5330752747299492752\n", + "-1906626916754175967->-2470839580670079044\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "3386821119738866121\n", + "4071937302134756076\n", "\n", "to_pyarrow_string\n", "\n", - "\n", + "\n", "\n", - "5330752747299492752->3386821119738866121\n", + "-2470839580670079044->4071937302134756076\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "5177257767402434271\n", + "5178095293817516608\n", "\n", "read-csv\n", "\n", - "\n", + "\n", "\n", - "7440036120417123049\n", + "4036801175431919381\n", "\n", "4\n", "\n", - "\n", + "\n", "\n", - "5177257767402434271->7440036120417123049\n", + "5178095293817516608->4036801175431919381\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "3425514041675701871\n", + "8508987607055959954\n", "\n", "to_pyarrow_string\n", "\n", - "\n", + "\n", "\n", - "7440036120417123049->3425514041675701871\n", + "4036801175431919381->8508987607055959954\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "-9030167133868224737\n", + "-9029329607453142400\n", "\n", "read-csv\n", "\n", - "\n", + "\n", "\n", - "2546962091444426683\n", + "-856272853540776985\n", "\n", "5\n", "\n", - "\n", + "\n", "\n", - "-9030167133868224737->2546962091444426683\n", + "-9029329607453142400->-856272853540776985\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "7664833214114594479\n", + "-2363636268343853305\n", "\n", "to_pyarrow_string\n", "\n", - "\n", + "\n", "\n", - "2546962091444426683->7664833214114594479\n", + "-856272853540776985->-2363636268343853305\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "-810036887397515834\n", + "8774990811659256194\n", "\n", "0\n", "\n", - "\n", + "\n", "\n", - "-1856934773411932769->-810036887397515834\n", + "6071941830101125281->8774990811659256194\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "5697603868704482591\n", + "3881916782686559828\n", "\n", "1\n", "\n", - "\n", + "\n", "\n", - "5227787437159759806->5697603868704482591\n", + "-8135483071169533727->3881916782686559828\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "804529839731786225\n", + "-8057186534920993363\n", "\n", "2\n", "\n", - "\n", + "\n", "\n", - "5068749226614284286->804529839731786225\n", + "-1050760860597841152->-8057186534920993363\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "2913813212849416522\n", + "1098126126831493759\n", "\n", "3\n", "\n", - "\n", + "\n", "\n", - "3386821119738866121->2913813212849416522\n", + "4071937302134756076->1098126126831493759\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "-9025290104758136669\n", + "7605766882933492184\n", "\n", "4\n", "\n", - "\n", + "\n", "\n", - "3425514041675701871->-9025290104758136669\n", + "8508987607055959954->7605766882933492184\n", "\n", "\n", "\n", - "\n", + "\n", "\n", - "4528379939978718581\n", + "-4333336434674061007\n", "\n", "5\n", "\n", - "\n", + "\n", "\n", - "7664833214114594479->4528379939978718581\n", + "-2363636268343853305->-4333336434674061007\n", "\n", "\n", "\n", @@ -724,7 +745,7 @@ "" ] }, - "execution_count": 19, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -758,42 +779,14 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 7, "metadata": {}, "outputs": [ { - "name": "stderr", + "name": "stdout", "output_type": "stream", "text": [ - "2024-02-09 17:43:50,091 - distributed.worker - WARNING - Compute Failed\n", - "Key: ('to_pyarrow_string-be40c5ef4347788c673803e766c9f5ac', 5)\n", - "Function: execute_task\n", - "args: ((subgraph_callable-9daf7b14-cf6d-4b93-b345-9197c71b1866, [(, , 0, 24979433, b'\\n'), None, True, True]))\n", - "kwargs: {}\n", - "Exception: 'ValueError(\\'Mismatched dtypes found in `pd.read_csv`/`pd.read_table`.\\\\n\\\\n+----------------+---------+----------+\\\\n| Column | Found | Expected |\\\\n+----------------+---------+----------+\\\\n| CRSElapsedTime | float64 | int64 |\\\\n| TailNum | object | float64 |\\\\n+----------------+---------+----------+\\\\n\\\\nThe following columns also raised exceptions on conversion:\\\\n\\\\n- TailNum\\\\n ValueError(\"could not convert string to float: \\\\\\'N14346\\\\\\'\")\\\\n\\\\nUsually this is due to dask\\\\\\'s dtype inference failing, and\\\\n*may* be fixed by specifying dtypes manually by adding:\\\\n\\\\ndtype={\\\\\\'CRSElapsedTime\\\\\\': \\\\\\'float64\\\\\\',\\\\n \\\\\\'TailNum\\\\\\': \\\\\\'object\\\\\\'}\\\\n\\\\nto the call to `read_csv`/`read_table`.\\')'\n", - "\n", - "Traceback (most recent call last):\n", - " File \"/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_35647/3690877535.py\", line 3, in \n", - " ddf.tail(3)\n", - " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/core.py\", line 1591, in tail\n", - " result = result.compute()\n", - " ^^^^^^^^^^^^^^^^\n", - " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/base.py\", line 342, in compute\n", - " (result,) = compute(self, traverse=False, **kwargs)\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/base.py\", line 628, in compute\n", - " results = schedule(dsk, keys, **kwargs)\n", - " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", - " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/io/csv.py\", line 142, in __call__\n", - " df = pandas_read_text(\n", - " ^^^^^^^^^^^^^^^^^\n", - " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/io/csv.py\", line 197, in pandas_read_text\n", - " coerce_dtypes(df, dtypes)\n", - " ^^^^^^^^^^^^^^^^^\n", - " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/io/csv.py\", line 298, in coerce_dtypes\n", - " raise ValueError(msg)\n", - " ^^^^^^^^^^^^^^^^^\n", - "ValueError: Mismatched dtypes found in `pd.read_csv`/`pd.read_table`.\n", + "ValueError, Mismatched dtypes found in `pd.read_csv`/`pd.read_table`.\n", "\n", "+----------------+---------+----------+\n", "| Column | Found | Expected |\n", @@ -815,14 +808,26 @@ "\n", "to the call to `read_csv`/`read_table`.\n" ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-02-20 13:26:56,131 - distributed.worker - WARNING - Compute Failed\n", + "Key: ('to_pyarrow_string-be40c5ef4347788c673803e766c9f5ac', 5)\n", + "Function: execute_task\n", + "args: ((subgraph_callable-104274d7-85ea-4720-954d-78db2282e8c8, [(, , 0, 24979433, b'\\n'), None, True, True]))\n", + "kwargs: {}\n", + "Exception: 'ValueError(\\'Mismatched dtypes found in `pd.read_csv`/`pd.read_table`.\\\\n\\\\n+----------------+---------+----------+\\\\n| Column | Found | Expected |\\\\n+----------------+---------+----------+\\\\n| CRSElapsedTime | float64 | int64 |\\\\n| TailNum | object | float64 |\\\\n+----------------+---------+----------+\\\\n\\\\nThe following columns also raised exceptions on conversion:\\\\n\\\\n- TailNum\\\\n ValueError(\"could not convert string to float: \\\\\\'N14346\\\\\\'\")\\\\n\\\\nUsually this is due to dask\\\\\\'s dtype inference failing, and\\\\n*may* be fixed by specifying dtypes manually by adding:\\\\n\\\\ndtype={\\\\\\'CRSElapsedTime\\\\\\': \\\\\\'float64\\\\\\',\\\\n \\\\\\'TailNum\\\\\\': \\\\\\'object\\\\\\'}\\\\n\\\\nto the call to `read_csv`/`read_table`.\\')'\n", + "\n" + ] } ], "source": [ - "import traceback\n", "try:\n", " ddf.tail(3)\n", - "except Exception:\n", - " traceback.print_exc()" + "except Exception as e:\n", + " print(f\"{type(e).__name__}, {e}\")" ] }, { @@ -836,7 +841,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -985,7 +990,7 @@ "[3 rows x 21 columns]" ] }, - "execution_count": 21, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -999,6 +1004,85 @@ "ddf.tail(3)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dask DataFrame 把各个字段的 Schema 放在了 `_meta` 里,包括字段的名称和类型。" + ] + }, + { + "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", + "
DateDayOfWeekDepTimeCRSDepTimeArrTimeCRSArrTimeUniqueCarrierFlightNumTailNumActualElapsedTime...AirTimeArrDelayDepDelayOriginDestDistanceTaxiInTaxiOutCancelledDiverted
\n", + "

0 rows × 21 columns

\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: [Date, DayOfWeek, DepTime, CRSDepTime, ArrTime, CRSArrTime, UniqueCarrier, FlightNum, TailNum, ActualElapsedTime, CRSElapsedTime, AirTime, ArrDelay, DepDelay, Origin, Dest, Distance, TaxiIn, TaxiOut, Cancelled, Diverted]\n", + "Index: []\n", + "\n", + "[0 rows x 21 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf._meta" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1008,7 +1092,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -1020,8 +1104,8 @@ "JFK 255485\n", "LGA 591305\n", "Name: Origin, dtype: int64\n", - "CPU times: user 1.77 s, sys: 701 ms, total: 2.47 s\n", - "Wall time: 5.62 s\n" + "CPU times: user 1.67 s, sys: 328 ms, total: 2 s\n", + "Wall time: 5.49 s\n" ] } ], @@ -1038,7 +1122,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -1050,8 +1134,8 @@ "JFK 255485\n", "LGA 591305\n", "Name: Origin, dtype: int64\n", - "CPU times: user 48.6 ms, sys: 16.1 ms, total: 64.6 ms\n", - "Wall time: 509 ms\n" + "CPU times: user 51.4 ms, sys: 11.1 ms, total: 62.5 ms\n", + "Wall time: 539 ms\n" ] } ], @@ -1080,32 +1164,52 @@ "\n", "具体而言,列式存储按照列进行存储,而不是 CSV 那样按行存储。数据分析时,我们可能只关心特定的列,而不是所有的列,因此在读取数据时,Parquet 允许很方便地过滤掉不需要的列,而不必读取整个行。因为减少了读取的数据量,Parquet 可以显著提高性能,也被广泛应用在 Apache Spark、Apache Hive 和 Apache Flink 等大数据生态。Parquet 自带了表模式,每个 Parquet 文件里嵌入了每个列的列名、数据类型等元数据,也就避免了 Dask DataFrame 进行表模式推测时推测不准确的问题。Parquet 中的数据是经过压缩的,相比 CSV,Parquet 更节省持久化存储的空间。\n", "\n", + "```{figure} ../img/ch-dask-dataframe/parquet.svg\n", + "---\n", + "width: 600px\n", + "name: parquet-with-row-group\n", + "---\n", + "Parquet 是一种列式存储,一个 Parquet 文件至少一个 Row Group,Row Group 中的数据按列存储,每个列存储在一个 Column Chunk 中。 \n", + "```\n", + "\n", "比如,应该尽量读取所需要的列,而不是所有的列。\n", "\n", "```python\n", "dd.read_parquet(\n", - " \"s3://path/to/parquet/\",\n", + " \"s3://path/to/folder/\",\n", " columns=[\"a\", \"b\", \"c\"]\n", ")\n", "```\n", "\n", - "此外,Parquet 提供了行分组(Row Group)的概念,如 {numref}`parquet-row-group` 所示,Parquet 文件中的数据是分组的,Row Group 定义了一个组内有多少行,这个例子中,一共 3 个 Row Group,每个 Row Group 有 2 行数据。每个 Row Group 存储了列的最大值和最小值等元数据,对这些列进行某些查询时,通过元数据的信息就可以确定是否读取这个 Row Group。比如,某时间列是一个时间序列,查询 “每天 9:00 至 12:00 的销量”,Row Group 中的元数据记录了时间列的最大值和最小值,通过元数据就可以判断是否有必要把这个 Row Group 读取出来,避免了读取不必要的开销。\n", - "\n", - "```{figure} ../img/ch-dask-dataframe/parquet-row-group.svg\n", - "---\n", - "width: 600px\n", - "name: parquet-row-group\n", - "---\n", - "Parquet 列式存储与 Row Group\n", - "```\n", + "此外,Parquet 提供了行分组(Row Group)的概念,如 {numref}`parquet-with-row-group` 所示,Parquet 文件中的数据是分组的,Row Group 定义了一个组内有多少行,这个例子中,一共 3 个 Row Group,每个 Row Group 有 2 行数据。每个 Row Group 存储了列的最大值和最小值等元数据,对这些列进行某些查询时,通过元数据的信息就可以确定是否读取这个 Row Group。比如,某时间列是一个时间序列,查询 “每天 9:00 至 12:00 的销量”,Row Group 中的元数据记录了时间列的最大值和最小值,通过元数据就可以判断是否有必要把这个 Row Group 读取出来,避免了读取不必要的开销。\n", "\n", "由于 Row Group 的引入,一个 Parquet 文件内可能有多个组,也就是说 Parquet 文件帮我们做了分组;不过很多 Parquet 文件的 Row Group 是 1。\n", "\n", "通常,数据集的文件被拆分为多个 Parquet,并且按照一定的方式来组织。比如,按照时间来切分:\n", "\n", "```\n", - "/path/folder/\n", - ".../year/month/day.parquet\n", + "path/to/folder/\n", + "├── year=2023/\n", + "│ ├── month=01/\n", + "│ │ └── part.0.parquet\n", + "│ └── month=02/\n", + "│ ├── part.0.parquet\n", + "│ └── part.1.parquet\n", + "└── year=2024/\n", + " └── month=01/\n", + " └── part.0.parquet\n", + "```\n", + "\n", + "可以过滤掉不需要的数据:\n", + "\n", + "```python\n", + "dd.read_parquet(\"path/to/folder/\", filters=[(\"year\", \">=\", 2023)])\n", + "```\n", + "\n", + "也可以根据这种目录结构存储数据:\n", + "\n", + "```python\n", + "df.to_parquet(\"path/to/folder/\", partition_on=[\"year\", \"month\"])\n", "```\n", "\n", "Dask DataFrame 在读取 Parquet 数据集时,除了根据文件数量外,又多了一个可能的依据:Row Group。如果有 m 个文件,那 Partition 可能的选项有:\n", @@ -1114,12 +1218,12 @@ "* 每个 Row Group 对应 Dask 的一个 Partition,假如一个 Parquet 文件有 n 个 Row Group,共有 m * n 个 Partition\n", "\n", "但无论哪种方式,应该尽量保证每个 Partition 所占用的内存空间不超过 Worker 的物理内存空间。Dask DataFrame 的\n", - "`read_parquet()` 通过 `split_row_groups` 参数给用户更多选项。默认为 `split_row_groups=\"infer\"`:Dask DataFrame 根据 Parquet 文件中的元数据来推测,是每个文件对应一个 Partition,还是每个 Row Group 对应一个 Partition;如果设置为 `split_row_groups=True` 则强制每个 Row Group 对应一个 Partition;如果设置为 `False`,则每个文件对应一个 Partition。" + "`read_parquet()` 通过 `split_row_groups` 参数给用户更多选项。如果设置为 `split_row_groups=True` 则强制每个 Row Group 对应一个 Partition;如果设置为 `False`,则每个文件对应一个 Partition。默认为 `split_row_groups=\"infer\"`:Dask DataFrame 根据读到的第一个 Parquet 文件的数据大小等元数据来推测,是每个文件对应一个 Partition,还是每个 Row Group 对应一个 Partition。" ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 15, "metadata": { "tags": [ "hide-cell" @@ -1129,6 +1233,13 @@ "source": [ "client.shutdown()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/ch-dask-dataframe/shuffle-test.ipynb b/ch-dask-dataframe/shuffle-test.ipynb new file mode 100644 index 0000000..63052e0 --- /dev/null +++ b/ch-dask-dataframe/shuffle-test.ipynb @@ -0,0 +1,1712 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(dask-dataframe-shuffle)=\n", + "# Shuffle\n", + "\n", + "在分布式场景下,`sort`,`merge`,`groupby` 有可能会在不同 Worker 之间交换数据,即 Shuffle。这些 pandas 算子在单机上实现起来比较简单,但是在大数据分布式计算场景,实现起来并不简单。\n", + "Dask 在 `2023.1` 版本之后提供了一种新的 Shuffle 方法,可以加速大部分计算任务。\n", + "\n", + "## `groupby`\n", + "\n", + "{numref}`dataframe-groupby` 展示了 `groupby` 在单机上的操作流程,它主要有三个阶段:分组、聚合、输出。分布式场景下,不同的数据分布在不同的 Partition 下。\n", + "\n", + "```{figure} ../img/ch-dask-dataframe/groupby.svg\n", + "---\n", + "width: 600px\n", + "name: dataframe-groupby\n", + "---\n", + "DataFrame groupby 示意图\n", + "```\n", + "\n", + "* `groupby(indexed_columns).agg()` 和 `groupby(indexed_columns).apply(user_def_fn)` 性能最好。`indexed_columns` 指的是索引列 Key,`agg` 指的是 Dask DataFrame 提供的官方的 `sum`,`mean`,`nunique` 等聚合方法。因为 `indexed_columns` 是排过序的了,可以很快地对 `indexed_columns` 进行分组,Shuffle 数据量不大。\n", + "* `groupby(non_indexed_columns).agg()` 的数据交换量要更大一些,`agg` 是 Dask 官方提供的方法,做过一些优化。\n", + "* `groupby(non_indexed_columns).apply(user_def_fn)` 的成本最高。它既要对所有数据进行交换,又要执行用户自定义的函数,\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import urllib\n", + "import shutil\n", + "import warnings\n", + "\n", + "warnings.simplefilter(action='ignore', category=FutureWarning)\n", + "\n", + "folder_path = os.path.join(os.getcwd(), \"../data/\")\n", + "download_url_prefix = \"https://gender-pay-gap.service.gov.uk/viewing/download-data/\"\n", + "file_path_prefix = os.path.join(folder_path, \"gender-pay\")\n", + "if not os.path.exists(file_path_prefix):\n", + " os.makedirs(file_path_prefix)\n", + "for year in [2017, 2018, 2019, 2020, 2021, 2022]:\n", + " download_url = download_url_prefix + str(year)\n", + " file_path = os.path.join(file_path_prefix, f\"{str(year)}.csv\")\n", + " if not os.path.exists(file_path):\n", + " with urllib.request.urlopen(download_url) as response, open(file_path, 'wb') as out_file:\n", + " shutil.copyfileobj(response, out_file)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import dask.dataframe as dd\n", + "import pandas as pd\n", + "from dask.distributed import LocalCluster, Client\n", + "\n", + "cluster = LocalCluster()\n", + "client = Client(cluster)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "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", + "
PostCodeEmployerSizeDiffMeanHourlyPercent
0DT11 0PX500 to 99918.0
1EH6 8NU250 to 4992.3
2LS7 1AB250 to 49941.0
3TA6 3JA250 to 499-22.0
4SR5 1SU250 to 49913.4
\n", + "
" + ], + "text/plain": [ + " PostCode EmployerSize DiffMeanHourlyPercent\n", + "0 DT11 0PX 500 to 999 18.0\n", + "1 EH6 8NU 250 to 499 2.3\n", + "2 LS7 1AB 250 to 499 41.0\n", + "3 TA6 3JA 250 to 499 -22.0\n", + "4 SR5 1SU 250 to 499 13.4" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf = dd.read_csv(os.path.join(file_path_prefix, \"*.csv\"),\n", + " dtype={'EmployerSize': 'str',\n", + " 'DiffMeanHourlyPercent': 'float64'})\n", + "\n", + "def fillna(df):\n", + " return df.fillna(value={\"PostCode\": \"UNKNOWN\"})\n", + "\n", + "ddf = ddf[[\"PostCode\", \"EmployerSize\", \"DiffMeanHourlyPercent\"]]\n", + "ddf = ddf.dropna()\n", + "# ddf = ddf.map_partitions(fillna)\n", + "ddf.head(5)\n", + " # .map_partitions(update_empsize_to_median)\n", + "# ddf = ddf.map_partitions(update_empsize_to_median)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PostCode string[pyarrow]\n", + "EmployerSize string[pyarrow]\n", + "DiffMeanHourlyPercent float64\n", + "dtype: object" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf.dtypes" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# ddf['EmployerSize']=ddf['EmployerSize'].astype(str)\n", + "def update_empsize_to_median(df):\n", + " def to_median(value):\n", + " if isinstance(value, str):\n", + " if \" to \" in value:\n", + " f , t = value.replace(\",\", \"\").split(\" to \")\n", + " return (int(f) + int(t)) / 2.0\n", + " elif \"Less than\" in value:\n", + " return 100\n", + " else:\n", + " return 10000\n", + " else:\n", + " return 0\n", + " df[\"EmployerSize\"] = df[\"EmployerSize\"].apply(to_median)\n", + " return df\n", + "\n", + "try:\n", + " ddf = ddf.map_partitions(update_empsize_to_median)\n", + "except Exception as e:\n", + " print(f\"{type(e).__name__}, {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "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", + "
PostCodeEmployerSizeDiffMeanHourlyPercentPostCodeLength
0DT11 0PX749.518.08
1EH6 8NU374.52.37
2LS7 1AB374.541.07
3TA6 3JA374.5-22.07
4SR5 1SU374.513.47
\n", + "
" + ], + "text/plain": [ + " PostCode EmployerSize DiffMeanHourlyPercent PostCodeLength\n", + "0 DT11 0PX 749.5 18.0 8\n", + "1 EH6 8NU 374.5 2.3 7\n", + "2 LS7 1AB 374.5 41.0 7\n", + "3 TA6 3JA 374.5 -22.0 7\n", + "4 SR5 1SU 374.5 13.4 7" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf['PostCodeLength'] = ddf['PostCode'].str.len()\n", + "ddf.head(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_49527/3631319032.py:14: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n" + ] + }, + { + "data": { + "text/plain": [ + "PostCode\n", + " BS34 7QH 7.375000\n", + " DE14 2EB 0.070000\n", + " DN15 6NL 3.666667\n", + " OX1 4BH 17.460000\n", + " S65 1EG 14.716667\n", + " ... \n", + "WS11 0DJ 14.600000\n", + "WS13 8EL -4.800000\n", + "WV10 8DS 24.330000\n", + "WV6 8DA 18.100000\n", + "YO25 8EJ 12.930000\n", + "Name: DiffMeanHourlyPercent, Length: 9118, dtype: float64" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "d = ddf.groupby('PostCode')['DiffMeanHourlyPercent'].mean()\n", + "d.compute()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "ename": "KeyError", + "evalue": "'EmployerSize'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/indexes/base.py:3805\u001b[0m, in \u001b[0;36mIndex.get_loc\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 3804\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 3805\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_engine\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_loc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcasted_key\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3806\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m err:\n", + "File \u001b[0;32mindex.pyx:167\u001b[0m, in \u001b[0;36mpandas._libs.index.IndexEngine.get_loc\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mindex.pyx:175\u001b[0m, in \u001b[0;36mpandas._libs.index.IndexEngine.get_loc\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mpandas/_libs/index_class_helper.pxi:70\u001b[0m, in \u001b[0;36mpandas._libs.index.Int64Engine._check_type\u001b[0;34m()\u001b[0m\n", + "\u001b[0;31mKeyError\u001b[0m: 'EmployerSize'", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[5], line 18\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m total \u001b[38;5;241m/\u001b[39m weights\n\u001b[1;32m 12\u001b[0m weighted_mean \u001b[38;5;241m=\u001b[39m dd\u001b[38;5;241m.\u001b[39mAggregation(\n\u001b[1;32m 13\u001b[0m name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mweighted_mean\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 14\u001b[0m chunk\u001b[38;5;241m=\u001b[39mprocess_chunk,\n\u001b[1;32m 15\u001b[0m agg\u001b[38;5;241m=\u001b[39magg,\n\u001b[1;32m 16\u001b[0m finalize\u001b[38;5;241m=\u001b[39mfinalize)\n\u001b[0;32m---> 18\u001b[0m aggregated \u001b[38;5;241m=\u001b[39m \u001b[43mddf\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgroupby\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mPostCode\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mEmployerSize\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mDiffMeanHourlyPercent\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43magg\u001b[49m\u001b[43m(\u001b[49m\u001b[43mweighted_mean\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 19\u001b[0m aggregated\u001b[38;5;241m.\u001b[39mhead(\u001b[38;5;241m10\u001b[39m)\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_groupby.py:1909\u001b[0m, in \u001b[0;36mGroupBy.agg\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1908\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21magg\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m-> 1909\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43maggregate\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_groupby.py:1893\u001b[0m, in \u001b[0;36mGroupBy.aggregate\u001b[0;34m(self, arg, split_every, split_out, shuffle_method, **kwargs)\u001b[0m\n\u001b[1;32m 1890\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m arg \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msize\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 1891\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msize()\n\u001b[0;32m-> 1893\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mnew_collection\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1894\u001b[0m \u001b[43m \u001b[49m\u001b[43mGroupbyAggregation\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1895\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexpr\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1896\u001b[0m \u001b[43m \u001b[49m\u001b[43marg\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1897\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mobserved\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1898\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdropna\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1899\u001b[0m \u001b[43m \u001b[49m\u001b[43msplit_every\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1900\u001b[0m \u001b[43m \u001b[49m\u001b[43msplit_out\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1901\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1902\u001b[0m \u001b[43m \u001b[49m\u001b[43mshuffle_method\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1903\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_slice\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1904\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mby\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1905\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1906\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_collection.py:4578\u001b[0m, in \u001b[0;36mnew_collection\u001b[0;34m(expr)\u001b[0m\n\u001b[1;32m 4576\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mnew_collection\u001b[39m(expr):\n\u001b[1;32m 4577\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Create new collection from an expr\"\"\"\u001b[39;00m\n\u001b[0;32m-> 4578\u001b[0m meta \u001b[38;5;241m=\u001b[39m \u001b[43mexpr\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_meta\u001b[49m\n\u001b[1;32m 4579\u001b[0m expr\u001b[38;5;241m.\u001b[39m_name \u001b[38;5;66;03m# Ensure backend is imported\u001b[39;00m\n\u001b[1;32m 4580\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m get_collection_type(meta)(expr)\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/functools.py:1001\u001b[0m, in \u001b[0;36mcached_property.__get__\u001b[0;34m(self, instance, owner)\u001b[0m\n\u001b[1;32m 999\u001b[0m val \u001b[38;5;241m=\u001b[39m cache\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mattrname, _NOT_FOUND)\n\u001b[1;32m 1000\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m val \u001b[38;5;129;01mis\u001b[39;00m _NOT_FOUND:\n\u001b[0;32m-> 1001\u001b[0m val \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstance\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1002\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1003\u001b[0m cache[\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mattrname] \u001b[38;5;241m=\u001b[39m val\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_groupby.py:431\u001b[0m, in \u001b[0;36mGroupbyAggregation._meta\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 429\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mcached_property\n\u001b[1;32m 430\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_meta\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[0;32m--> 431\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_lower\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_meta\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/functools.py:1001\u001b[0m, in \u001b[0;36mcached_property.__get__\u001b[0;34m(self, instance, owner)\u001b[0m\n\u001b[1;32m 999\u001b[0m val \u001b[38;5;241m=\u001b[39m cache\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mattrname, _NOT_FOUND)\n\u001b[1;32m 1000\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m val \u001b[38;5;129;01mis\u001b[39;00m _NOT_FOUND:\n\u001b[0;32m-> 1001\u001b[0m val \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstance\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1002\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1003\u001b[0m cache[\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mattrname] \u001b[38;5;241m=\u001b[39m val\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_reductions.py:429\u001b[0m, in \u001b[0;36mApplyConcatApply._meta\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 427\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mcached_property\n\u001b[1;32m 428\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_meta\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[0;32m--> 429\u001b[0m meta \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_meta_chunk\u001b[49m\n\u001b[1;32m 430\u001b[0m aggregate \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39maggregate \u001b[38;5;129;01mor\u001b[39;00m (\u001b[38;5;28;01mlambda\u001b[39;00m x: x)\n\u001b[1;32m 431\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcombine:\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/functools.py:1001\u001b[0m, in \u001b[0;36mcached_property.__get__\u001b[0;34m(self, instance, owner)\u001b[0m\n\u001b[1;32m 999\u001b[0m val \u001b[38;5;241m=\u001b[39m cache\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mattrname, _NOT_FOUND)\n\u001b[1;32m 1000\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m val \u001b[38;5;129;01mis\u001b[39;00m _NOT_FOUND:\n\u001b[0;32m-> 1001\u001b[0m val \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43minstance\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1002\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1003\u001b[0m cache[\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mattrname] \u001b[38;5;241m=\u001b[39m val\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_groupby.py:205\u001b[0m, in \u001b[0;36mGroupByApplyConcatApply._meta_chunk\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 202\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mcached_property\n\u001b[1;32m 203\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_meta_chunk\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m 204\u001b[0m meta \u001b[38;5;241m=\u001b[39m meta_nonempty(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mframe\u001b[38;5;241m.\u001b[39m_meta)\n\u001b[0;32m--> 205\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mchunk\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmeta\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_by_meta\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mchunk_kwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py:1200\u001b[0m, in \u001b[0;36m_groupby_apply_funcs\u001b[0;34m(df, *by, **kwargs)\u001b[0m\n\u001b[1;32m 1198\u001b[0m result \u001b[38;5;241m=\u001b[39m collections\u001b[38;5;241m.\u001b[39mOrderedDict()\n\u001b[1;32m 1199\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m result_column, func, func_kwargs \u001b[38;5;129;01min\u001b[39;00m funcs:\n\u001b[0;32m-> 1200\u001b[0m r \u001b[38;5;241m=\u001b[39m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mgrouped\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mfunc_kwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1202\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(r, \u001b[38;5;28mtuple\u001b[39m):\n\u001b[1;32m 1203\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m idx, s \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(r):\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py:1278\u001b[0m, in \u001b[0;36m_apply_func_to_column\u001b[0;34m(df_like, column, func)\u001b[0m\n\u001b[1;32m 1275\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m column \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 1276\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m func(df_like)\n\u001b[0;32m-> 1278\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdf_like\u001b[49m\u001b[43m[\u001b[49m\u001b[43mcolumn\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[5], line 4\u001b[0m, in \u001b[0;36mprocess_chunk\u001b[0;34m(chunk)\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mweighted_func\u001b[39m(df):\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m (df[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEmployerSize\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m*\u001b[39m df[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mDiffMeanHourlyPercent\u001b[39m\u001b[38;5;124m\"\u001b[39m])\u001b[38;5;241m.\u001b[39msum()\n\u001b[0;32m----> 4\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m (\u001b[43mchunk\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply\u001b[49m\u001b[43m(\u001b[49m\u001b[43mweighted_func\u001b[49m\u001b[43m)\u001b[49m, chunk[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEmployerSize\u001b[39m\u001b[38;5;124m\"\u001b[39m]\u001b[38;5;241m.\u001b[39msum())\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/groupby/generic.py:230\u001b[0m, in \u001b[0;36mSeriesGroupBy.apply\u001b[0;34m(self, func, *args, **kwargs)\u001b[0m\n\u001b[1;32m 224\u001b[0m \u001b[38;5;129m@Appender\u001b[39m(\n\u001b[1;32m 225\u001b[0m _apply_docs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtemplate\u001b[39m\u001b[38;5;124m\"\u001b[39m]\u001b[38;5;241m.\u001b[39mformat(\n\u001b[1;32m 226\u001b[0m \u001b[38;5;28minput\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mseries\u001b[39m\u001b[38;5;124m\"\u001b[39m, examples\u001b[38;5;241m=\u001b[39m_apply_docs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mseries_examples\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n\u001b[1;32m 227\u001b[0m )\n\u001b[1;32m 228\u001b[0m )\n\u001b[1;32m 229\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mapply\u001b[39m(\u001b[38;5;28mself\u001b[39m, func, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Series:\n\u001b[0;32m--> 230\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/groupby/groupby.py:1824\u001b[0m, in \u001b[0;36mGroupBy.apply\u001b[0;34m(self, func, include_groups, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1822\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m option_context(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmode.chained_assignment\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m 1823\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 1824\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_python_apply_general\u001b[49m\u001b[43m(\u001b[49m\u001b[43mf\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_selected_obj\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1825\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (\n\u001b[1;32m 1826\u001b[0m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mobj, Series)\n\u001b[1;32m 1827\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_selection \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 1828\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_selected_obj\u001b[38;5;241m.\u001b[39mshape \u001b[38;5;241m!=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_obj_with_exclusions\u001b[38;5;241m.\u001b[39mshape\n\u001b[1;32m 1829\u001b[0m ):\n\u001b[1;32m 1830\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\n\u001b[1;32m 1831\u001b[0m message\u001b[38;5;241m=\u001b[39m_apply_groupings_depr\u001b[38;5;241m.\u001b[39mformat(\n\u001b[1;32m 1832\u001b[0m \u001b[38;5;28mtype\u001b[39m(\u001b[38;5;28mself\u001b[39m)\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__name__\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mapply\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1835\u001b[0m stacklevel\u001b[38;5;241m=\u001b[39mfind_stack_level(),\n\u001b[1;32m 1836\u001b[0m )\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/groupby/groupby.py:1885\u001b[0m, in \u001b[0;36mGroupBy._python_apply_general\u001b[0;34m(self, f, data, not_indexed_same, is_transform, is_agg)\u001b[0m\n\u001b[1;32m 1850\u001b[0m \u001b[38;5;129m@final\u001b[39m\n\u001b[1;32m 1851\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_python_apply_general\u001b[39m(\n\u001b[1;32m 1852\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1857\u001b[0m is_agg: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[1;32m 1858\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m NDFrameT:\n\u001b[1;32m 1859\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1860\u001b[0m \u001b[38;5;124;03m Apply function f in python space\u001b[39;00m\n\u001b[1;32m 1861\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1883\u001b[0m \u001b[38;5;124;03m data after applying f\u001b[39;00m\n\u001b[1;32m 1884\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 1885\u001b[0m values, mutated \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_grouper\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mapply_groupwise\u001b[49m\u001b[43m(\u001b[49m\u001b[43mf\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43maxis\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1886\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m not_indexed_same \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 1887\u001b[0m not_indexed_same \u001b[38;5;241m=\u001b[39m mutated\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/groupby/ops.py:919\u001b[0m, in \u001b[0;36mBaseGrouper.apply_groupwise\u001b[0;34m(self, f, data, axis)\u001b[0m\n\u001b[1;32m 917\u001b[0m \u001b[38;5;66;03m# group might be modified\u001b[39;00m\n\u001b[1;32m 918\u001b[0m group_axes \u001b[38;5;241m=\u001b[39m group\u001b[38;5;241m.\u001b[39maxes\n\u001b[0;32m--> 919\u001b[0m res \u001b[38;5;241m=\u001b[39m \u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[43mgroup\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 920\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m mutated \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m _is_indexed_like(res, group_axes, axis):\n\u001b[1;32m 921\u001b[0m mutated \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n", + "Cell \u001b[0;32mIn[5], line 3\u001b[0m, in \u001b[0;36mprocess_chunk..weighted_func\u001b[0;34m(df)\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mweighted_func\u001b[39m(df):\n\u001b[0;32m----> 3\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m (\u001b[43mdf\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mEmployerSize\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m \u001b[38;5;241m*\u001b[39m df[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mDiffMeanHourlyPercent\u001b[39m\u001b[38;5;124m\"\u001b[39m])\u001b[38;5;241m.\u001b[39msum()\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/series.py:1112\u001b[0m, in \u001b[0;36mSeries.__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 1109\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_values[key]\n\u001b[1;32m 1111\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m key_is_scalar:\n\u001b[0;32m-> 1112\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_get_value\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1114\u001b[0m \u001b[38;5;66;03m# Convert generator to list before going through hashable part\u001b[39;00m\n\u001b[1;32m 1115\u001b[0m \u001b[38;5;66;03m# (We will iterate through the generator there to check for slices)\u001b[39;00m\n\u001b[1;32m 1116\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m is_iterator(key):\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/series.py:1228\u001b[0m, in \u001b[0;36mSeries._get_value\u001b[0;34m(self, label, takeable)\u001b[0m\n\u001b[1;32m 1225\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_values[label]\n\u001b[1;32m 1227\u001b[0m \u001b[38;5;66;03m# Similar to Index.get_value, but we do not fall back to positional\u001b[39;00m\n\u001b[0;32m-> 1228\u001b[0m loc \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mindex\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_loc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mlabel\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1230\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m is_integer(loc):\n\u001b[1;32m 1231\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_values[loc]\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/indexes/base.py:3812\u001b[0m, in \u001b[0;36mIndex.get_loc\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 3807\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(casted_key, \u001b[38;5;28mslice\u001b[39m) \u001b[38;5;129;01mor\u001b[39;00m (\n\u001b[1;32m 3808\u001b[0m \u001b[38;5;28misinstance\u001b[39m(casted_key, abc\u001b[38;5;241m.\u001b[39mIterable)\n\u001b[1;32m 3809\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28many\u001b[39m(\u001b[38;5;28misinstance\u001b[39m(x, \u001b[38;5;28mslice\u001b[39m) \u001b[38;5;28;01mfor\u001b[39;00m x \u001b[38;5;129;01min\u001b[39;00m casted_key)\n\u001b[1;32m 3810\u001b[0m ):\n\u001b[1;32m 3811\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m InvalidIndexError(key)\n\u001b[0;32m-> 3812\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(key) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01merr\u001b[39;00m\n\u001b[1;32m 3813\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m:\n\u001b[1;32m 3814\u001b[0m \u001b[38;5;66;03m# If we have a listlike key, _check_indexing_error will raise\u001b[39;00m\n\u001b[1;32m 3815\u001b[0m \u001b[38;5;66;03m# InvalidIndexError. Otherwise we fall through and re-raise\u001b[39;00m\n\u001b[1;32m 3816\u001b[0m \u001b[38;5;66;03m# the TypeError.\u001b[39;00m\n\u001b[1;32m 3817\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_check_indexing_error(key)\n", + "\u001b[0;31mKeyError\u001b[0m: 'EmployerSize'" + ] + } + ], + "source": [ + "def process_chunk(chunk):\n", + " def weighted_func(df):\n", + " return (df[\"EmployerSize\"] * df[\"DiffMeanHourlyPercent\"]).sum()\n", + " return (chunk.apply(weighted_func), chunk[\"EmployerSize\"].sum())\n", + "\n", + "def agg(total, weights):\n", + " return (total.sum(), weights.sum())\n", + "\n", + "def finalize(total, weights):\n", + " return total / weights\n", + " \n", + "weighted_mean = dd.Aggregation(\n", + " name='weighted_mean',\n", + " chunk=process_chunk,\n", + " agg=agg,\n", + " finalize=finalize)\n", + "\n", + "aggregated = ddf.groupby(\"PostCode\")[\"EmployerSize\", \"DiffMeanHourlyPercent\"].agg(weighted_mean)\n", + "aggregated.head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_49527/3631319032.py:14: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n" + ] + }, + { + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
PostCodeEmployerSizeDiffMeanHourlyPercentPostCodeLength
0DT11 0PX749.518.08
1EH6 8NU374.52.37
2LS7 1AB374.541.07
3TA6 3JA374.5-22.07
4SR5 1SU374.513.47
...............
10838SN1 1AP2999.525.97
10839PO15 7JZ2999.516.48
10840SK11 0LP374.518.98
10841SY5 0BD749.520.07
10842SE1 2AU374.53.77
\n", + "

58849 rows × 4 columns

\n", + "
" + ], + "text/plain": [ + " PostCode EmployerSize DiffMeanHourlyPercent PostCodeLength\n", + "0 DT11 0PX 749.5 18.0 8\n", + "1 EH6 8NU 374.5 2.3 7\n", + "2 LS7 1AB 374.5 41.0 7\n", + "3 TA6 3JA 374.5 -22.0 7\n", + "4 SR5 1SU 374.5 13.4 7\n", + "... ... ... ... ...\n", + "10838 SN1 1AP 2999.5 25.9 7\n", + "10839 PO15 7JZ 2999.5 16.4 8\n", + "10840 SK11 0LP 374.5 18.9 8\n", + "10841 SY5 0BD 749.5 20.0 7\n", + "10842 SE1 2AU 374.5 3.7 7\n", + "\n", + "[58849 rows x 4 columns]" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf.groupby(\"PostCode\")['DiffMeanHourlyPercent'].sum()\n", + "d = ddf.compute()\n", + "d" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "ename": "ValueError", + "evalue": "Metadata inference failed in `_groupby_apply_funcs`.\n\nYou have supplied a custom function and Dask is unable to \ndetermine the type of output that that function returns. \n\nTo resolve this please provide a meta= keyword.\nThe docstring of the Dask function you ran should have more information.\n\nOriginal error is below:\n------------------------\nIndexError('Column(s) DiffMeanHourlyPercent already selected')\n\nTraceback:\n---------\n File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/utils.py\", line 194, in raise_on_meta_error\n yield\n File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/core.py\", line 7057, in _emulate\n return func(*_extract_meta(args, True), **_extract_meta(kwargs, True))\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py\", line 1194, in _groupby_apply_funcs\n r = func(grouped, **func_kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py\", line 1240, in _apply_func_to_column\n return func(df_like[column])\n ^^^^^^^^^^^^^^^^^^^^^\n File \"/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_50604/416441874.py\", line 3, in \n chunk=lambda s: (s['EmployerSize'].count(), s['DiffMeanHourlyPercent'].sum()),\n ~^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/base.py\", line 234, in __getitem__\n raise IndexError(f\"Column(s) {self._selection} already selected\")\n", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/utils.py:194\u001b[0m, in \u001b[0;36mraise_on_meta_error\u001b[0;34m(funcname, udf)\u001b[0m\n\u001b[1;32m 193\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 194\u001b[0m \u001b[38;5;28;01myield\u001b[39;00m\n\u001b[1;32m 195\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/core.py:7057\u001b[0m, in \u001b[0;36m_emulate\u001b[0;34m(func, udf, *args, **kwargs)\u001b[0m\n\u001b[1;32m 7056\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m raise_on_meta_error(funcname(func), udf\u001b[38;5;241m=\u001b[39mudf), check_numeric_only_deprecation():\n\u001b[0;32m-> 7057\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m_extract_meta\u001b[49m\u001b[43m(\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m_extract_meta\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py:1194\u001b[0m, in \u001b[0;36m_groupby_apply_funcs\u001b[0;34m(df, *by, **kwargs)\u001b[0m\n\u001b[1;32m 1193\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m result_column, func, func_kwargs \u001b[38;5;129;01min\u001b[39;00m funcs:\n\u001b[0;32m-> 1194\u001b[0m r \u001b[38;5;241m=\u001b[39m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mgrouped\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mfunc_kwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1196\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(r, \u001b[38;5;28mtuple\u001b[39m):\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py:1240\u001b[0m, in \u001b[0;36m_apply_func_to_column\u001b[0;34m(df_like, column, func)\u001b[0m\n\u001b[1;32m 1238\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m func(df_like)\n\u001b[0;32m-> 1240\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdf_like\u001b[49m\u001b[43m[\u001b[49m\u001b[43mcolumn\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[10], line 3\u001b[0m, in \u001b[0;36m\u001b[0;34m(s)\u001b[0m\n\u001b[1;32m 1\u001b[0m custom_mean \u001b[38;5;241m=\u001b[39m dd\u001b[38;5;241m.\u001b[39mAggregation(\n\u001b[1;32m 2\u001b[0m name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mcustom_mean\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[0;32m----> 3\u001b[0m chunk\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mlambda\u001b[39;00m s: (\u001b[43ms\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mEmployerSize\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[38;5;241m.\u001b[39mcount(), s[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mDiffMeanHourlyPercent\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39msum()),\n\u001b[1;32m 4\u001b[0m agg\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mlambda\u001b[39;00m count, \u001b[38;5;28msum\u001b[39m: (count\u001b[38;5;241m.\u001b[39msum(), \u001b[38;5;28msum\u001b[39m\u001b[38;5;241m.\u001b[39msum()),\n\u001b[1;32m 5\u001b[0m finalize\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mlambda\u001b[39;00m count, \u001b[38;5;28msum\u001b[39m: \u001b[38;5;28msum\u001b[39m \u001b[38;5;241m/\u001b[39m count,\n\u001b[1;32m 6\u001b[0m ) \n\u001b[1;32m 7\u001b[0m a \u001b[38;5;241m=\u001b[39m ddf\u001b[38;5;241m.\u001b[39mgroupby(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mPostCode\u001b[39m\u001b[38;5;124m'\u001b[39m)\u001b[38;5;241m.\u001b[39magg(custom_mean)\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/base.py:234\u001b[0m, in \u001b[0;36mSelectionMixin.__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 233\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_selection \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m--> 234\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mIndexError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mColumn(s) \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_selection\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m already selected\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 236\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(key, (\u001b[38;5;28mlist\u001b[39m, \u001b[38;5;28mtuple\u001b[39m, ABCSeries, ABCIndex, np\u001b[38;5;241m.\u001b[39mndarray)):\n", + "\u001b[0;31mIndexError\u001b[0m: Column(s) DiffMeanHourlyPercent already selected", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[10], line 7\u001b[0m\n\u001b[1;32m 1\u001b[0m custom_mean \u001b[38;5;241m=\u001b[39m dd\u001b[38;5;241m.\u001b[39mAggregation(\n\u001b[1;32m 2\u001b[0m name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mcustom_mean\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 3\u001b[0m chunk\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mlambda\u001b[39;00m s: (s[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mEmployerSize\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39mcount(), s[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mDiffMeanHourlyPercent\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39msum()),\n\u001b[1;32m 4\u001b[0m agg\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mlambda\u001b[39;00m count, \u001b[38;5;28msum\u001b[39m: (count\u001b[38;5;241m.\u001b[39msum(), \u001b[38;5;28msum\u001b[39m\u001b[38;5;241m.\u001b[39msum()),\n\u001b[1;32m 5\u001b[0m finalize\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mlambda\u001b[39;00m count, \u001b[38;5;28msum\u001b[39m: \u001b[38;5;28msum\u001b[39m \u001b[38;5;241m/\u001b[39m count,\n\u001b[1;32m 6\u001b[0m ) \n\u001b[0;32m----> 7\u001b[0m a \u001b[38;5;241m=\u001b[39m \u001b[43mddf\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgroupby\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mPostCode\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43magg\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcustom_mean\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 8\u001b[0m a\u001b[38;5;241m.\u001b[39mhead(\u001b[38;5;241m5\u001b[39m) \n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py:352\u001b[0m, in \u001b[0;36mnumeric_only_not_implemented..wrapper\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 342\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (\n\u001b[1;32m 343\u001b[0m PANDAS_GE_150\n\u001b[1;32m 344\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m PANDAS_GE_200\n\u001b[1;32m 345\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m numeric_only \u001b[38;5;129;01mis\u001b[39;00m no_default\n\u001b[1;32m 346\u001b[0m ):\n\u001b[1;32m 347\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\n\u001b[1;32m 348\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mThe default value of numeric_only will be changed to False \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 349\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124min the future when using dask with pandas 2.0\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 350\u001b[0m \u001b[38;5;167;01mFutureWarning\u001b[39;00m,\n\u001b[1;32m 351\u001b[0m )\n\u001b[0;32m--> 352\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py:2928\u001b[0m, in \u001b[0;36mDataFrameGroupBy.agg\u001b[0;34m(self, arg, split_every, split_out, shuffle, **kwargs)\u001b[0m\n\u001b[1;32m 2925\u001b[0m \u001b[38;5;129m@_aggregate_docstring\u001b[39m(based_on\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mpd.core.groupby.DataFrameGroupBy.agg\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 2926\u001b[0m \u001b[38;5;129m@numeric_only_not_implemented\u001b[39m\n\u001b[1;32m 2927\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21magg\u001b[39m(\u001b[38;5;28mself\u001b[39m, arg\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, split_every\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, split_out\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m, shuffle\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m-> 2928\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43maggregate\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2929\u001b[0m \u001b[43m \u001b[49m\u001b[43marg\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43marg\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2930\u001b[0m \u001b[43m \u001b[49m\u001b[43msplit_every\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msplit_every\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2931\u001b[0m \u001b[43m \u001b[49m\u001b[43msplit_out\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msplit_out\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2932\u001b[0m \u001b[43m \u001b[49m\u001b[43mshuffle\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mshuffle\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2933\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2934\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py:2917\u001b[0m, in \u001b[0;36mDataFrameGroupBy.aggregate\u001b[0;34m(self, arg, split_every, split_out, shuffle, **kwargs)\u001b[0m\n\u001b[1;32m 2914\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m arg \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msize\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 2915\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msize()\n\u001b[0;32m-> 2917\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43maggregate\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2918\u001b[0m \u001b[43m \u001b[49m\u001b[43marg\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43marg\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2919\u001b[0m \u001b[43m \u001b[49m\u001b[43msplit_every\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msplit_every\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2920\u001b[0m \u001b[43m \u001b[49m\u001b[43msplit_out\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msplit_out\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2921\u001b[0m \u001b[43m \u001b[49m\u001b[43mshuffle\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mshuffle\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2922\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2923\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py:2381\u001b[0m, in \u001b[0;36m_GroupBy.aggregate\u001b[0;34m(self, arg, split_every, split_out, shuffle, **kwargs)\u001b[0m\n\u001b[1;32m 2374\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msort \u001b[38;5;129;01mand\u001b[39;00m split_out \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m 2375\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mNotImplementedError\u001b[39;00m(\n\u001b[1;32m 2376\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCannot guarantee sorted keys for `split_out>1` and `shuffle=False`\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 2377\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m Try using `shuffle=True` if you are grouping on a single column.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 2378\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m Otherwise, try using split_out=1, or grouping with sort=False.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 2379\u001b[0m )\n\u001b[0;32m-> 2381\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43maca\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2382\u001b[0m \u001b[43m \u001b[49m\u001b[43mchunk_args\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2383\u001b[0m \u001b[43m \u001b[49m\u001b[43mchunk\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m_groupby_apply_funcs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2384\u001b[0m \u001b[43m \u001b[49m\u001b[43mchunk_kwargs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mdict\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2385\u001b[0m \u001b[43m \u001b[49m\u001b[43mfuncs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mchunk_funcs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2386\u001b[0m \u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[1;32m 2387\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mobserved\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2388\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdropna\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2389\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2390\u001b[0m \u001b[43m \u001b[49m\u001b[43mcombine\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m_groupby_apply_funcs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2391\u001b[0m \u001b[43m \u001b[49m\u001b[43mcombine_kwargs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mdict\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2392\u001b[0m \u001b[43m \u001b[49m\u001b[43mfuncs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43maggregate_funcs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2393\u001b[0m \u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlevels\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2394\u001b[0m \u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[1;32m 2395\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mobserved\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2396\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdropna\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2397\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2398\u001b[0m \u001b[43m \u001b[49m\u001b[43maggregate\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m_agg_finalize\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2399\u001b[0m \u001b[43m \u001b[49m\u001b[43maggregate_kwargs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mdict\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2400\u001b[0m \u001b[43m \u001b[49m\u001b[43maggregate_funcs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43maggregate_funcs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2401\u001b[0m \u001b[43m \u001b[49m\u001b[43mfinalize_funcs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfinalizers\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2402\u001b[0m \u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlevels\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2403\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mobserved\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2404\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdropna\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2405\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2406\u001b[0m \u001b[43m \u001b[49m\u001b[43mtoken\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43maggregate\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2407\u001b[0m \u001b[43m \u001b[49m\u001b[43msplit_every\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msplit_every\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2408\u001b[0m \u001b[43m \u001b[49m\u001b[43msplit_out\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msplit_out\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2409\u001b[0m \u001b[43m \u001b[49m\u001b[43msplit_out_setup\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msplit_out_on_index\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2410\u001b[0m \u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2411\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2413\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m relabeling \u001b[38;5;129;01mand\u001b[39;00m result \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 2414\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m order \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/core.py:7010\u001b[0m, in \u001b[0;36mapply_concat_apply\u001b[0;34m(args, chunk, aggregate, combine, meta, token, chunk_kwargs, aggregate_kwargs, combine_kwargs, split_every, split_out, split_out_setup, split_out_setup_kwargs, sort, ignore_index, **kwargs)\u001b[0m\n\u001b[1;32m 6995\u001b[0m layer \u001b[38;5;241m=\u001b[39m DataFrameTreeReduction(\n\u001b[1;32m 6996\u001b[0m final_name,\n\u001b[1;32m 6997\u001b[0m chunked\u001b[38;5;241m.\u001b[39mname,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 7006\u001b[0m tree_node_name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtoken\u001b[38;5;250m \u001b[39m\u001b[38;5;129;01mor\u001b[39;00m\u001b[38;5;250m \u001b[39mfuncname(combine)\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m-combine-\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtoken_key\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 7007\u001b[0m )\n\u001b[1;32m 7009\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m meta \u001b[38;5;129;01mis\u001b[39;00m no_default:\n\u001b[0;32m-> 7010\u001b[0m meta_chunk \u001b[38;5;241m=\u001b[39m \u001b[43m_emulate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mchunk\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mudf\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mchunk_kwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 7011\u001b[0m meta \u001b[38;5;241m=\u001b[39m _emulate(\n\u001b[1;32m 7012\u001b[0m aggregate, _concat([meta_chunk], ignore_index), udf\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39maggregate_kwargs\n\u001b[1;32m 7013\u001b[0m )\n\u001b[1;32m 7014\u001b[0m meta \u001b[38;5;241m=\u001b[39m make_meta(\n\u001b[1;32m 7015\u001b[0m meta,\n\u001b[1;32m 7016\u001b[0m index\u001b[38;5;241m=\u001b[39m(\u001b[38;5;28mgetattr\u001b[39m(make_meta(dfs[\u001b[38;5;241m0\u001b[39m]), \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mindex\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;28;01mif\u001b[39;00m dfs \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m),\n\u001b[1;32m 7017\u001b[0m parent_meta\u001b[38;5;241m=\u001b[39mdfs[\u001b[38;5;241m0\u001b[39m]\u001b[38;5;241m.\u001b[39m_meta,\n\u001b[1;32m 7018\u001b[0m )\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/core.py:7056\u001b[0m, in \u001b[0;36m_emulate\u001b[0;34m(func, udf, *args, **kwargs)\u001b[0m\n\u001b[1;32m 7051\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_emulate\u001b[39m(func, \u001b[38;5;241m*\u001b[39margs, udf\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 7052\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 7053\u001b[0m \u001b[38;5;124;03m Apply a function using args / kwargs. If arguments contain dd.DataFrame /\u001b[39;00m\n\u001b[1;32m 7054\u001b[0m \u001b[38;5;124;03m dd.Series, using internal cache (``_meta``) for calculation\u001b[39;00m\n\u001b[1;32m 7055\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 7056\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mwith\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mraise_on_meta_error\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfuncname\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mudf\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mudf\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcheck_numeric_only_deprecation\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 7057\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mreturn\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m_extract_meta\u001b[49m\u001b[43m(\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m_extract_meta\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/contextlib.py:158\u001b[0m, in \u001b[0;36m_GeneratorContextManager.__exit__\u001b[0;34m(self, typ, value, traceback)\u001b[0m\n\u001b[1;32m 156\u001b[0m value \u001b[38;5;241m=\u001b[39m typ()\n\u001b[1;32m 157\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 158\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgen\u001b[38;5;241m.\u001b[39mthrow(typ, value, traceback)\n\u001b[1;32m 159\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mStopIteration\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[1;32m 160\u001b[0m \u001b[38;5;66;03m# Suppress StopIteration *unless* it's the same exception that\u001b[39;00m\n\u001b[1;32m 161\u001b[0m \u001b[38;5;66;03m# was passed to throw(). This prevents a StopIteration\u001b[39;00m\n\u001b[1;32m 162\u001b[0m \u001b[38;5;66;03m# raised inside the \"with\" statement from being suppressed.\u001b[39;00m\n\u001b[1;32m 163\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m exc \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m value\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/utils.py:215\u001b[0m, in \u001b[0;36mraise_on_meta_error\u001b[0;34m(funcname, udf)\u001b[0m\n\u001b[1;32m 206\u001b[0m msg \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m (\n\u001b[1;32m 207\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mOriginal error is below:\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 208\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m------------------------\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 212\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{2}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 213\u001b[0m )\n\u001b[1;32m 214\u001b[0m msg \u001b[38;5;241m=\u001b[39m msg\u001b[38;5;241m.\u001b[39mformat(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m in `\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfuncname\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m`\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m funcname \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28mrepr\u001b[39m(e), tb)\n\u001b[0;32m--> 215\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(msg) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n", + "\u001b[0;31mValueError\u001b[0m: Metadata inference failed in `_groupby_apply_funcs`.\n\nYou have supplied a custom function and Dask is unable to \ndetermine the type of output that that function returns. \n\nTo resolve this please provide a meta= keyword.\nThe docstring of the Dask function you ran should have more information.\n\nOriginal error is below:\n------------------------\nIndexError('Column(s) DiffMeanHourlyPercent already selected')\n\nTraceback:\n---------\n File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/utils.py\", line 194, in raise_on_meta_error\n yield\n File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/core.py\", line 7057, in _emulate\n return func(*_extract_meta(args, True), **_extract_meta(kwargs, True))\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py\", line 1194, in _groupby_apply_funcs\n r = func(grouped, **func_kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py\", line 1240, in _apply_func_to_column\n return func(df_like[column])\n ^^^^^^^^^^^^^^^^^^^^^\n File \"/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_50604/416441874.py\", line 3, in \n chunk=lambda s: (s['EmployerSize'].count(), s['DiffMeanHourlyPercent'].sum()),\n ~^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/base.py\", line 234, in __getitem__\n raise IndexError(f\"Column(s) {self._selection} already selected\")\n" + ] + } + ], + "source": [ + "custom_mean = dd.Aggregation(\n", + " name='custom_mean',\n", + " chunk=lambda s: (s['EmployerSize'].count(), s['DiffMeanHourlyPercent'].sum()),\n", + " agg=lambda count, sum: (count.sum(), sum.sum()),\n", + " finalize=lambda count, sum: sum / count,\n", + ") \n", + "a = ddf.groupby('PostCode').agg(custom_mean)\n", + "a.head(5) " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "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", + " \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", + "
EmployerNameEmployerIdAddressPostCodeCompanyNumberSicCodesDiffMeanHourlyPercentDiffMedianHourlyPercentDiffMeanBonusPercentDiffMedianBonusPercent...FemaleUpperMiddleQuartileMaleTopQuartileFemaleTopQuartileCompanyLinkToGPGInfoResponsiblePersonEmployerSizeCurrentNameSubmittedAfterTheDeadlineDueDateDateSubmitted
1\"RED BAND\" CHEMICAL COMPANY, LIMITED1687919 Smith's Place, Leith Walk, Edinburgh, EH6 8NUEH6 8NUSC016876477302.3-2.715.037.5...89.718.181.9<NA>Philip Galt (Managing Director)250 to 499\"RED BAND\" CHEMICAL COMPANY, LIMITEDFalse2018/04/05 00:00:002018/03/28 16:44:25
2123 EMPLOYEES LTD1767734 Roundhay Road, Leeds, England, LS7 1ABLS7 1AB105306517830041.036.0-69.8-157.2...89.023.077.0<NA>Chloe Lines (Financial Controller)250 to 499123 EMPLOYEES LTDTrue2018/04/05 00:00:002018/05/04 11:24:06
71STOP HALAL LIMITED689Colmore Court, 9 Colmore Row, Birmingham, West...B3 2BJ089290705629011.90.00.00.0...41.969.830.2<NA>Stephen Elder (Finance Director)250 to 499SHAZAN FOODS LIMITEDFalse2018/04/05 00:00:002018/03/22 08:08:33
\n", + "

3 rows × 27 columns

\n", + "
" + ], + "text/plain": [ + " EmployerName EmployerId \\\n", + "1 \"RED BAND\" CHEMICAL COMPANY, LIMITED 16879 \n", + "2 123 EMPLOYEES LTD 17677 \n", + "7 1STOP HALAL LIMITED 689 \n", + "\n", + " Address PostCode CompanyNumber \\\n", + "1 19 Smith's Place, Leith Walk, Edinburgh, EH6 8NU EH6 8NU SC016876 \n", + "2 34 Roundhay Road, Leeds, England, LS7 1AB LS7 1AB 10530651 \n", + "7 Colmore Court, 9 Colmore Row, Birmingham, West... B3 2BJ 08929070 \n", + "\n", + " SicCodes DiffMeanHourlyPercent DiffMedianHourlyPercent \\\n", + "1 47730 2.3 -2.7 \n", + "2 78300 41.0 36.0 \n", + "7 56290 11.9 0.0 \n", + "\n", + " DiffMeanBonusPercent DiffMedianBonusPercent ... \\\n", + "1 15.0 37.5 ... \n", + "2 -69.8 -157.2 ... \n", + "7 0.0 0.0 ... \n", + "\n", + " FemaleUpperMiddleQuartile MaleTopQuartile FemaleTopQuartile \\\n", + "1 89.7 18.1 81.9 \n", + "2 89.0 23.0 77.0 \n", + "7 41.9 69.8 30.2 \n", + "\n", + " CompanyLinkToGPGInfo ResponsiblePerson EmployerSize \\\n", + "1 Philip Galt (Managing Director) 250 to 499 \n", + "2 Chloe Lines (Financial Controller) 250 to 499 \n", + "7 Stephen Elder (Finance Director) 250 to 499 \n", + "\n", + " CurrentName SubmittedAfterTheDeadline \\\n", + "1 \"RED BAND\" CHEMICAL COMPANY, LIMITED False \n", + "2 123 EMPLOYEES LTD True \n", + "7 SHAZAN FOODS LIMITED False \n", + "\n", + " DueDate DateSubmitted \n", + "1 2018/04/05 00:00:00 2018/03/28 16:44:25 \n", + "2 2018/04/05 00:00:00 2018/05/04 11:24:06 \n", + "7 2018/04/05 00:00:00 2018/03/22 08:08:33 \n", + "\n", + "[3 rows x 27 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "na_rows.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Dask DataFrame Structure:
\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", + " \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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
DateDayOfWeekDepTimeCRSDepTimeArrTimeCRSArrTimeUniqueCarrierFlightNumTailNumActualElapsedTimeCRSElapsedTimeAirTimeArrDelayDepDelayOriginDestDistanceTaxiInTaxiOutCancelledDiverted
npartitions=6
datetime64[ns]int64float64int64float64int64stringint64float64float64int64float64float64float64stringstringfloat64float64float64int64int64
...............................................................
..................................................................
...............................................................
...............................................................
\n", + "
\n", + "
Dask Name: try_loc, 3 graph layers
" + ], + "text/plain": [ + "Dask DataFrame Structure:\n", + " Date DayOfWeek DepTime CRSDepTime ArrTime CRSArrTime UniqueCarrier FlightNum TailNum ActualElapsedTime CRSElapsedTime AirTime ArrDelay DepDelay Origin Dest Distance TaxiIn TaxiOut Cancelled Diverted\n", + "npartitions=6 \n", + " datetime64[ns] int64 float64 int64 float64 int64 string int64 float64 float64 int64 float64 float64 float64 string string float64 float64 float64 int64 int64\n", + " ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...\n", + "... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...\n", + " ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...\n", + " ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...\n", + "Dask Name: try_loc, 3 graph layers" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def process_chunk(chunk):\n", + " def weighted_func(df):\n", + " return (df[\"EmployerSize\"] * df[\"DiffMeanHourlyPercent\"]).sum()\n", + " return (chunk.apply(weighted_func), chunk.sum()[\"EmployerSize\"])\n", + " \n", + "def agg(total, weights):\n", + " return (total.sum(), weights.sum())\n", + "\n", + "def finalize(total, weights):\n", + " return total / weights\n", + " \n", + "weighted_mean = dd.Aggregation(\n", + " name='weighted_mean',\n", + " chunk=process_chunk,\n", + " agg=agg,\n", + " finalize=finalize)\n", + "\n", + "aggregated = ddf.groupby(\"PostCode\")[\"EmployerSize\", \"DiffMeanHourlyPercent\"].agg(weighted_mean)" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [], + "source": [ + "client.shutdown()" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "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", + "
PostCodeEmployerSizeDiffMeanHourlyPercent
0A1000.1
1A2000.2
2B3000.3
3B4000.4
\n", + "
" + ], + "text/plain": [ + " PostCode EmployerSize DiffMeanHourlyPercent\n", + "0 A 100 0.1\n", + "1 A 200 0.2\n", + "2 B 300 0.3\n", + "3 B 400 0.4" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data = {'PostCode': ['A', 'A', 'B', 'B'],\n", + " 'EmployerSize': [100, 200, 300, 400],\n", + " 'DiffMeanHourlyPercent': [0.1, 0.2, 0.3, 0.4]}\n", + "df = pd.DataFrame(data)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1000" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.sum()[\"EmployerSize\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1000" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['EmployerSize'].sum()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "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", + "
PostCodeEmployerSizeDiffMeanHourlyPercent
0A1000.1
1A2000.2
2B3000.3
3B4000.4
\n", + "
" + ], + "text/plain": [ + " PostCode EmployerSize DiffMeanHourlyPercent\n", + "0 A 100 0.1\n", + "1 A 200 0.2\n", + "2 B 300 0.3\n", + "3 B 400 0.4" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data = {'PostCode': ['A', 'A', 'B', 'B'], 'EmployerSize': [100, 200, 300, 400], 'DiffMeanHourlyPercent': [0.1, 0.2, 0.3, 0.4]}\n", + "\n", + "df = pd.DataFrame(data)\n", + "ddf = dd.from_pandas(df, 2)\n", + "ddf.compute()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " PostCode EmployerSize DiffMeanHourlyPercent\n", + "0 A 100 0.1\n", + "1 A 200 0.2\n", + " PostCode EmployerSize DiffMeanHourlyPercent\n", + "2 B 300 0.3\n", + "3 B 400 0.4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_65513/3023763576.py:5: DeprecationWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.\n", + " result = df.groupby('PostCode').apply(weighted_mean).reset_index(name='WeightedMean')\n" + ] + }, + { + "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", + "
PostCodeWeightedMean
0A0.166667
1B0.357143
\n", + "
" + ], + "text/plain": [ + " PostCode WeightedMean\n", + "0 A 0.166667\n", + "1 B 0.357143" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def weighted_mean(data):\n", + " print(data)\n", + " return (data['EmployerSize'] * data['DiffMeanHourlyPercent']).sum() / data['EmployerSize'].sum()\n", + "\n", + "result = df.groupby('PostCode').apply(weighted_mean).reset_index(name='WeightedMean')\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PostCode\n", + "A 0.166667\n", + "B 0.357143\n", + "dtype: float64" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[\"Weighted\"] = df[\"EmployerSize\"] * df[\"DiffMeanHourlyPercent\"]\n", + "df = df.groupby(\"PostCode\").sum()\n", + "df = df[\"Weighted\"] / df[\"EmployerSize\"]\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "ename": "KeyError", + "evalue": "\"Columns not found: 'EmployerSize'\"", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[49], line 15\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m total \u001b[38;5;241m/\u001b[39m weights\n\u001b[1;32m 14\u001b[0m extent \u001b[38;5;241m=\u001b[39m dd\u001b[38;5;241m.\u001b[39mAggregation(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mextent\u001b[39m\u001b[38;5;124m'\u001b[39m, chunk, agg, finalize\u001b[38;5;241m=\u001b[39mfinalize)\n\u001b[0;32m---> 15\u001b[0m \u001b[43mddf\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgroupby\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mPostCode\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mEmployerSize\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mDiffMeanHourlyPercent\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[38;5;241m.\u001b[39magg(extent)\u001b[38;5;241m.\u001b[39mcompute()\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask/dataframe/groupby.py:2885\u001b[0m, in \u001b[0;36mDataFrameGroupBy.__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 2883\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(key, \u001b[38;5;28mtuple\u001b[39m):\n\u001b[1;32m 2884\u001b[0m key \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(key)\n\u001b[0;32m-> 2885\u001b[0m g\u001b[38;5;241m.\u001b[39m_meta \u001b[38;5;241m=\u001b[39m \u001b[43mg\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_meta\u001b[49m\u001b[43m[\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m]\u001b[49m\n\u001b[1;32m 2886\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m g\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/groupby/generic.py:1964\u001b[0m, in \u001b[0;36mDataFrameGroupBy.__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 1957\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(key, \u001b[38;5;28mtuple\u001b[39m) \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(key) \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m 1958\u001b[0m \u001b[38;5;66;03m# if len == 1, then it becomes a SeriesGroupBy and this is actually\u001b[39;00m\n\u001b[1;32m 1959\u001b[0m \u001b[38;5;66;03m# valid syntax, so don't raise\u001b[39;00m\n\u001b[1;32m 1960\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 1961\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCannot subset columns with a tuple with more than one element. \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 1962\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mUse a list instead.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 1963\u001b[0m )\n\u001b[0;32m-> 1964\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;21;43m__getitem__\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/base.py:239\u001b[0m, in \u001b[0;36mSelectionMixin.__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 237\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mobj\u001b[38;5;241m.\u001b[39mcolumns\u001b[38;5;241m.\u001b[39mintersection(key)) \u001b[38;5;241m!=\u001b[39m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mset\u001b[39m(key)):\n\u001b[1;32m 238\u001b[0m bad_keys \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(\u001b[38;5;28mset\u001b[39m(key)\u001b[38;5;241m.\u001b[39mdifference(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mobj\u001b[38;5;241m.\u001b[39mcolumns))\n\u001b[0;32m--> 239\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mColumns not found: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mstr\u001b[39m(bad_keys)[\u001b[38;5;241m1\u001b[39m:\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 240\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_gotitem(\u001b[38;5;28mlist\u001b[39m(key), ndim\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m2\u001b[39m)\n\u001b[1;32m 242\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n", + "\u001b[0;31mKeyError\u001b[0m: \"Columns not found: 'EmployerSize'\"" + ] + } + ], + "source": [ + "def chunk(chunk):\n", + " def weighted_func(df):\n", + " return (df[\"EmployerSize\"] * df[\"DiffMeanHourlyPercent\"]).sum()\n", + " return (chunk.apply(weighted_func), chunk.sum()[\"EmployerSize\"])\n", + "\n", + "def agg(total, weights):\n", + " return (total.sum(), weights.sum())\n", + " # return chunk_maxes.max(), chunk_mins.min()\n", + "\n", + "def finalize(total, weights):\n", + " return total / weights\n", + "\n", + "extent = dd.Aggregation('extent', chunk, agg, finalize=finalize)\n", + "ddf.groupby(\"PostCode\")['EmployerSize', 'DiffMeanHourlyPercent'].agg(extent).compute()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame({\n", + " 'PostCode': ['a', 'b', 'a', 'a', 'b', 'c', 'd'],\n", + " 'Size': [0, 1, 0, 2, 5, 3, 4],\n", + " 'DiffMeanHourlyPercent': [0.1, 0.2, 0.3, 0.4, 0.5, 0.1, 0.2],\n", + "})\n", + "ddf = dd.from_pandas(df, 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " count mean std min 25% 50% 75% max\n", + "PostCode \n", + "a 1.0 1.0 NaN 1.0 1.0 1.0 1.0 1.0\n", + " count mean std min 25% 50% 75% max\n", + "PostCode \n", + "a 1.0 1.0 NaN 1.0 1.0 1.0 1.0 1.0\n", + " count mean std min 25% 50% 75% max\n", + "PostCode \n", + "a 1.0 1.0 NaN 1.0 1.0 1.0 1.0 1.0\n", + " count mean std min 25% 50% 75% max\n", + "PostCode \n", + "a 1.0 1.0 NaN 1.0 1.0 1.0 1.0 1.0\n", + " count mean std min 25% 50% 75% max\n", + "PostCode \n", + "a 1.0 1.0 NaN 1.0 1.0 1.0 1.0 1.0\n", + " count mean std min 25% 50% 75% max\n", + "PostCode \n", + "a 1.0 1.0 NaN 1.0 1.0 1.0 1.0 1.0\n", + " count mean std min 25% 50% 75% max\n", + "PostCode \n", + "a 3.0 0.266667 0.152753 0.1 0.200 0.30 0.350 0.4\n", + "b 2.0 0.350000 0.212132 0.2 0.275 0.35 0.425 0.5\n", + "c 1.0 0.100000 NaN 0.1 0.100 0.10 0.100 0.1\n", + "d 1.0 0.200000 NaN 0.2 0.200 0.20 0.200 0.2\n", + " count mean std min 25% 50% 75% max\n", + "PostCode \n", + "a 3.0 0.666667 1.154701 0.0 0.0 0.0 1.0 2.0\n", + "b 2.0 3.000000 2.828427 1.0 2.0 3.0 4.0 5.0\n", + "c 1.0 3.000000 NaN 3.0 3.0 3.0 3.0 3.0\n", + "d 1.0 4.000000 NaN 4.0 4.0 4.0 4.0 4.0\n" + ] + }, + { + "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", + "
SizeDiffMeanHourlyPercent
PostCode
a20.3
b40.3
c00.0
d00.0
\n", + "
" + ], + "text/plain": [ + " Size DiffMeanHourlyPercent\n", + "PostCode \n", + "a 2 0.3\n", + "b 4 0.3\n", + "c 0 0.0\n", + "d 0 0.0" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-03-20 13:47:52,849 - tornado.application - ERROR - Exception in callback >\n", + "Traceback (most recent call last):\n", + " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/tornado/ioloop.py\", line 937, in _run\n", + " val = self.callback()\n", + " ^^^^^^^^^^^^^^^\n", + " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/distributed/system_monitor.py\", line 167, in update\n", + " net_ioc = psutil.net_io_counters()\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/psutil/__init__.py\", line 2126, in net_io_counters\n", + " rawdict = _psplatform.net_io_counters()\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "OSError: [Errno 12] Cannot allocate memory\n", + "2024-03-20 19:55:47,781 - tornado.application - ERROR - Exception in callback >\n", + "Traceback (most recent call last):\n", + " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/tornado/ioloop.py\", line 937, in _run\n", + " val = self.callback()\n", + " ^^^^^^^^^^^^^^^\n", + " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/distributed/system_monitor.py\", line 167, in update\n", + " net_ioc = psutil.net_io_counters()\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/psutil/__init__.py\", line 2126, in net_io_counters\n", + " rawdict = _psplatform.net_io_counters()\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "OSError: [Errno 12] Cannot allocate memory\n" + ] + } + ], + "source": [ + "def chunk(grouped):\n", + " print(grouped.describe())\n", + " return grouped.max(), grouped.min()\n", + "\n", + "def agg(chunk_maxes, chunk_mins):\n", + " return chunk_maxes.max(), chunk_mins.min()\n", + "\n", + "def finalize(maxima, minima):\n", + " return maxima - minima\n", + "\n", + "extent = dd.Aggregation('extent', chunk, agg, finalize=finalize)\n", + "ddf.groupby('PostCode')[\"Size\", \"DiffMeanHourlyPercent\"].agg(extent).compute()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame({\n", + " 'A': ['a', 'b', 'a', 'a', 'b'],\n", + " 'B': [0, 1, 0, 2, 5],\n", + "})\n", + "ddf = dd.from_pandas(df, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def chunk(grouped):\n", + " print(grouped)\n", + " return grouped.max(), grouped.min()\n", + "\n", + "def agg(chunk_maxes, chunk_mins):\n", + " return chunk_maxes.max(), chunk_mins.min()\n", + "\n", + "def finalize(maxima, minima):\n", + " return maxima - minima" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "ename": "KeyError", + "evalue": "'A'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[11], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m extent \u001b[38;5;241m=\u001b[39m dd\u001b[38;5;241m.\u001b[39mAggregation(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mextent\u001b[39m\u001b[38;5;124m'\u001b[39m, chunk, agg, finalize\u001b[38;5;241m=\u001b[39mfinalize)\n\u001b[0;32m----> 2\u001b[0m \u001b[43mddf\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgroupby\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mA\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39magg(extent)\u001b[38;5;241m.\u001b[39mcompute()\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_collection.py:2840\u001b[0m, in \u001b[0;36mDataFrame.groupby\u001b[0;34m(self, by, group_keys, sort, observed, dropna, **kwargs)\u001b[0m\n\u001b[1;32m 2835\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(by, FrameBase) \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(by, Series):\n\u001b[1;32m 2836\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 2837\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m`by` must be a column name or list of columns, got \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mby\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 2838\u001b[0m )\n\u001b[0;32m-> 2840\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mGroupBy\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2841\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2842\u001b[0m \u001b[43m \u001b[49m\u001b[43mby\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2843\u001b[0m \u001b[43m \u001b[49m\u001b[43mgroup_keys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mgroup_keys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2844\u001b[0m \u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2845\u001b[0m \u001b[43m \u001b[49m\u001b[43mobserved\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mobserved\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2846\u001b[0m \u001b[43m \u001b[49m\u001b[43mdropna\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdropna\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2847\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2848\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/dask_expr/_groupby.py:1519\u001b[0m, in \u001b[0;36mGroupBy.__init__\u001b[0;34m(self, obj, by, group_keys, sort, observed, dropna, slice)\u001b[0m\n\u001b[1;32m 1513\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mby \u001b[38;5;241m=\u001b[39m (\n\u001b[1;32m 1514\u001b[0m [by]\n\u001b[1;32m 1515\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m np\u001b[38;5;241m.\u001b[39misscalar(by) \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(by, Expr) \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(by, Callable)\n\u001b[1;32m 1516\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28mlist\u001b[39m(by)\n\u001b[1;32m 1517\u001b[0m )\n\u001b[1;32m 1518\u001b[0m \u001b[38;5;66;03m# surface pandas errors\u001b[39;00m\n\u001b[0;32m-> 1519\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_meta \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_meta\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgroupby\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1520\u001b[0m \u001b[43m \u001b[49m\u001b[43mby\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1521\u001b[0m \u001b[43m \u001b[49m\u001b[43mgroup_keys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mgroup_keys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1522\u001b[0m \u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1523\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m_as_dict\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mobserved\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mobserved\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1524\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m_as_dict\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mdropna\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdropna\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1525\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1526\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mslice\u001b[39m \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 1527\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mslice\u001b[39m, \u001b[38;5;28mtuple\u001b[39m):\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/frame.py:9170\u001b[0m, in \u001b[0;36mDataFrame.groupby\u001b[0;34m(self, by, axis, level, as_index, sort, group_keys, observed, dropna)\u001b[0m\n\u001b[1;32m 9167\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m level \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m by \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 9168\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mYou have to supply one of \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mby\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m and \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mlevel\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m-> 9170\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mDataFrameGroupBy\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 9171\u001b[0m \u001b[43m \u001b[49m\u001b[43mobj\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9172\u001b[0m \u001b[43m \u001b[49m\u001b[43mkeys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mby\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9173\u001b[0m \u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43maxis\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9174\u001b[0m \u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlevel\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9175\u001b[0m \u001b[43m \u001b[49m\u001b[43mas_index\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mas_index\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9176\u001b[0m \u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9177\u001b[0m \u001b[43m \u001b[49m\u001b[43mgroup_keys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mgroup_keys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9178\u001b[0m \u001b[43m \u001b[49m\u001b[43mobserved\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mobserved\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9179\u001b[0m \u001b[43m \u001b[49m\u001b[43mdropna\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdropna\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9180\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/groupby/groupby.py:1329\u001b[0m, in \u001b[0;36mGroupBy.__init__\u001b[0;34m(self, obj, keys, axis, level, grouper, exclusions, selection, as_index, sort, group_keys, observed, dropna)\u001b[0m\n\u001b[1;32m 1326\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdropna \u001b[38;5;241m=\u001b[39m dropna\n\u001b[1;32m 1328\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m grouper \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m-> 1329\u001b[0m grouper, exclusions, obj \u001b[38;5;241m=\u001b[39m \u001b[43mget_grouper\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1330\u001b[0m \u001b[43m \u001b[49m\u001b[43mobj\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1331\u001b[0m \u001b[43m \u001b[49m\u001b[43mkeys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1332\u001b[0m \u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43maxis\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1333\u001b[0m \u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlevel\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1334\u001b[0m \u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1335\u001b[0m \u001b[43m \u001b[49m\u001b[43mobserved\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mobserved\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mis\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mlib\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mno_default\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mobserved\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1336\u001b[0m \u001b[43m \u001b[49m\u001b[43mdropna\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdropna\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1337\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1339\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m observed \u001b[38;5;129;01mis\u001b[39;00m lib\u001b[38;5;241m.\u001b[39mno_default:\n\u001b[1;32m 1340\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28many\u001b[39m(ping\u001b[38;5;241m.\u001b[39m_passed_categorical \u001b[38;5;28;01mfor\u001b[39;00m ping \u001b[38;5;129;01min\u001b[39;00m grouper\u001b[38;5;241m.\u001b[39mgroupings):\n", + "File \u001b[0;32m~/miniconda3/envs/dispy/lib/python3.11/site-packages/pandas/core/groupby/grouper.py:1043\u001b[0m, in \u001b[0;36mget_grouper\u001b[0;34m(obj, key, axis, level, sort, observed, validate, dropna)\u001b[0m\n\u001b[1;32m 1041\u001b[0m in_axis, level, gpr \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m, gpr, \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 1042\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 1043\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(gpr)\n\u001b[1;32m 1044\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(gpr, Grouper) \u001b[38;5;129;01mand\u001b[39;00m gpr\u001b[38;5;241m.\u001b[39mkey \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 1045\u001b[0m \u001b[38;5;66;03m# Add key to exclusions\u001b[39;00m\n\u001b[1;32m 1046\u001b[0m exclusions\u001b[38;5;241m.\u001b[39madd(gpr\u001b[38;5;241m.\u001b[39mkey)\n", + "\u001b[0;31mKeyError\u001b[0m: 'A'" + ] + } + ], + "source": [ + "extent = dd.Aggregation('extent', chunk, agg, finalize=finalize)\n", + "ddf.groupby('A').agg(extent).compute()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "client.shutdown()" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "df = pd.DataFrame({\n", + " 'A': ['foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'foo'],\n", + " 'B': ['one', 'one', 'two', 'three', 'two', 'two', 'one', 'three'],\n", + " 'C': [1, 2, 3, 4, 5, 6, 7, 8],\n", + " 'D': [10, 20, 30, 40, 50, 60, 70, 80]\n", + "})\n", + "\n", + "# 根据 'A' 列进行分组\n", + "grouped = df.groupby('A')\n", + "print(grouped)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'client' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[2], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mclient\u001b[49m\u001b[38;5;241m.\u001b[39mshutdown()\n", + "\u001b[0;31mNameError\u001b[0m: name 'client' is not defined" + ] + } + ], + "source": [ + "client.shutdown()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dispy", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ch-dask-dataframe/shuffle.ipynb b/ch-dask-dataframe/shuffle.ipynb new file mode 100644 index 0000000..e7f9bc5 --- /dev/null +++ b/ch-dask-dataframe/shuffle.ipynb @@ -0,0 +1,59 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(dask-dataframe-shuffle)=\n", + "# Shuffle\n", + "\n", + "在分布式场景下,`sort`,`merge`,`groupby` 有可能会在不同 Worker 之间交换数据,即 Shuffle。这些 pandas 算子在单机上实现起来比较简单,但是在大数据分布式计算场景,实现起来并不简单。\n", + "Dask 在 `2023.1` 版本之后提供了一种新的 Shuffle 方法,可以加速大部分计算任务。\n", + "\n", + "## `groupby`\n", + "\n", + "{numref}`dataframe-groupby` 展示了 `groupby` 在单机上的操作流程,它主要有三个阶段:分组、聚合、输出。分布式场景下,不同的数据分布在不同的 Partition 下。\n", + "\n", + "```{figure} ../img/ch-dask-dataframe/groupby.svg\n", + "---\n", + "width: 600px\n", + "name: dataframe-groupby\n", + "---\n", + "DataFrame groupby 示意图\n", + "```\n", + "\n", + "* `groupby(indexed_columns).agg()` 和 `groupby(indexed_columns).apply(user_def_fn)` 性能最好。`indexed_columns` 指的是索引列 Key,`agg` 指的是 Dask DataFrame 提供的官方的 `sum`,`mean`,`nunique` 等聚合方法。因为 `indexed_columns` 是排过序的了,可以很快地对 `indexed_columns` 进行分组,Shuffle 数据量不大。\n", + "* `groupby(non_indexed_columns).agg()` 的数据交换量要更大一些,`agg` 是 Dask 官方提供的方法,做过一些优化。\n", + "* `groupby(non_indexed_columns).apply(user_def_fn)` 的成本最高。它既要对所有数据进行交换,又要执行用户自定义的函数,\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dispy", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ch-dask/dask-distributed.ipynb b/ch-dask/dask-distributed.ipynb index 387a0c7..45f1830 100644 --- a/ch-dask/dask-distributed.ipynb +++ b/ch-dask/dask-distributed.ipynb @@ -467,7 +467,7 @@ "* 用户当前作业对计算资源要求很高,需要更多的资源来满足计算需求。\n", "* 用户当前作业所申请的计算资源闲置,这些资源可被其他用户使用。尤其是当用户进行交互式数据可视化,而非大规模计算时。\n", "\n", - "比如 `KubeCluster` 和 `SLURMCluster` 都提供了 `adapt()` 方法。下面的例子可以在 0 到 10 个 Worker 之间动态缩放。自动缩放根据 Dask Scheduler 上作业的负载来决定运行多少个 Dask Worker。Dask 收集一些信息,比如每个 Dask Worker 上已用内存和可用内存,并根据这些信息自适应地调整计算资源数量。\n", + "`KubeCluster` 和 `SLURMCluster` 都提供了 `adapt()` 方法。下面的例子可以在 0 到 10 个 Worker 之间动态缩放。自动缩放根据 Dask Scheduler 上作业的负载来决定运行多少个 Dask Worker。Dask 收集一些信息,比如每个 Dask Worker 上已用内存和可用内存,并根据这些信息自适应地调整计算资源数量。\n", "\n", "```python\n", "from dask_kubernetes import KubeCluster\n", @@ -486,7 +486,9 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "client.shutdown()" + ] } ], "metadata": { diff --git a/ch-dask/task-graph-partitioning.ipynb b/ch-dask/task-graph-partitioning.ipynb index bbf88ae..ff5085d 100644 --- a/ch-dask/task-graph-partitioning.ipynb +++ b/ch-dask/task-graph-partitioning.ipynb @@ -532,7 +532,7 @@ "\n", "我们需要关注 Task Stream 栏,应该避免大量的空白或大量的红色。空白表示 Dask Worker 上没有任何任务,红色表示 Dask Worker 之间进行大量的数据交换。\n", "\n", - "{numref}`dask-good-partitions` 和 {numref}`dask-too-many-partitions` 是一个对比,两张图使用代码相同({numref}`dask-read-write` 中的例子),但是使用的数据块大小不同。{numref}`dask-too-many-partitions` 中的数据块过小,Task Graph 过大,出现了大量的红色,时间没有用在计算上,而是浪费在数据交换等其他事情上。\n", + "{numref}`dask-good-partitions` 和 {numref}`dask-too-many-partitions` 是一个对比,两张图使用代码相同({numref}`dask-dataframe-read-write` 中的例子),但是使用的数据块大小不同。{numref}`dask-too-many-partitions` 中的数据块过小,Task Graph 过大,出现了大量的红色,时间没有用在计算上,而是浪费在数据交换等其他事情上。\n", "\n", "```{figure} ../img/ch-dask/good-partitions.png\n", "---\n", diff --git a/ch-data-science/machine-learning.ipynb b/ch-data-science/machine-learning.ipynb index f1ede8b..fed1068 100644 --- a/ch-data-science/machine-learning.ipynb +++ b/ch-data-science/machine-learning.ipynb @@ -1056,6 +1056,44 @@ "* 学习率,即刚才提到的 $\\alpha$,控制着每次更新参数的步长。\n", "* 网络结构:模型的层数、每层的神经元数量、激活函数的选择等。不同的网络结构对于不同的任务可能有不同的性能表现。\n", "\n", + "## 实现细节\n", + "\n", + "神经网络训练实现起来要关注以下三个步骤:\n", + "\n", + "* 一次前向传播\n", + "* 一次反向传播\n", + "* 一次更新模型权重\n", + "\n", + "{numref}`model-training-input-output` 整理了神经网络的第 i 层进行训练时,以上三个步骤的输入和输出。\n", + "\n", + "```{figure} ../img/ch-data-science/model-training-input-output.svg\n", + "---\n", + "width: 800px\n", + "name: model-training-input-output\n", + "---\n", + "前向传播、反向传播和更新模型权重的输入和输出\n", + "```\n", + "\n", + "对于前向传播,输入有两部分:i-1 层输出 $\\boldsymbol{a^{[i-1]}}$ 和第 i 层的模型权重 $\\boldsymbol{W^{[i]}}$、$\\boldsymbol{b^{[i]}}$;输出又被称为激活(Activation)。\n", + "\n", + "对于反向传播,输入有三部分:i 层输出 $\\boldsymbol{a^{[i]}}$;第 i 层的模型权重 $\\boldsymbol{W^{[i]}}$、$\\boldsymbol{b^{[i]}}$;损失对 i 层输出的导数 $\\boldsymbol{\\boldsymbol{\\frac{\\partial L}{a^{[i]}}}}$。根据链式法则,可以求出损失对 i 层模型权重的导数 $\\boldsymbol{\\frac{\\partial L}{\\partial W^{[i]}\n", + "}}$、$\\boldsymbol{\\frac{\\partial L}{\\partial b^{[i]}\n", + "}}$,也就是梯度。\n", + "\n", + "得到梯度后,需要沿着梯度下降的方向更新模型权重。如果是最简单的梯度下降法,优化器直接在模型原有权重基础上做减法,不需要额外保存状态,比如:$\\boldsymbol{W^{[l]}} = \\boldsymbol{W^{[l]}}-\\alpha\\frac{\\partial L}{\\partial \\boldsymbol{W^{[l]}}}$\n", + "\n", + "复杂一点的优化器,比如 Adam, 在梯度下降时引入了动量的概念。动量是梯度的指数移动平均,需要维护一个梯度的移动平均矩阵,这个矩阵就是优化器的状态。因此,优化器状态、原来的模型权重和梯度共同作为输入,可以得到更新后的模型权重。至此才能完成一轮模型的训练。\n", + "\n", + "如果只考虑前向传播和反向传播,对于一个神经网络,其训练过程如 {numref}`model-training` 所示。{numref}`model-training` 演示了 3 层神经网络,前向过程用 FWD 表示,反向过程用 BWD 表示。\n", + "\n", + "```{figure} ../img/ch-data-science/model-training.svg\n", + "---\n", + "width: 800px\n", + "name: model-training\n", + "---\n", + "前向传播(图中用 FWD 表示)和反向传播(图中用 BWD 表示)\n", + "```\n", + "\n", "## 推理\n", "\n", "模型训练就是前向和反向传播,模型推理只需要前向传播,只不过输入层换成了需要预测的 $\\boldsymbol{x}$。" diff --git a/ch-mpi-large-model/data-parallel.md b/ch-mpi-large-model/data-parallel.md index 17ebe32..8f6fde2 100644 --- a/ch-mpi-large-model/data-parallel.md +++ b/ch-mpi-large-model/data-parallel.md @@ -1,6 +1,57 @@ -数据并行性是一种允许您通过在多个计算节点之间复制模型并在它们之间划分数据集来更快地训练模型。 +(data-parallel)= +# 数据并行 -首先对于一次顺序训练,其中我们有一个计算节点,该节点将我们的模型加载到内存中。在每次训练迭代期间,我们加载下一个小批量,并在缓存每层输出的同时对模型执行前向传递。然后,我们计算损失并运行向后传递,从而计算梯度。这个过程如下图所示,使用 MNIST 图像作为我们的示例输入数据。 -![[数据并行图1.png]] -在数据并行(DataParallel)中,需要再N台机器上赋值模型,我们分割我们的数据为N个块,并且让每台机器处理一个块。![[数据并行图2.png]]对于整个过程,首先在初始对于两个计算节点,需要将模型参数scatter到进程组中的其他进程,之后对于每个计算节点,一旦计算出梯度,都需要进行MPI.AllReduce,收集全部的信息来更新参数。 -![[数据并行图3.png]] \ No newline at end of file +数据并行是一种最常见的大模型并行方法,相对其他并行,数据并行最简单。如 {numref}`data-parallel-img` 所示,模型被拷贝到不同的 GPU 设备上,训练数据被切分为多份,每份分给不同的 GPU 进行训练。 + +```{figure} ../img/ch-mpi-large-model/data-parallel.svg +--- +width: 600px +name: data-parallel-img +--- +数据并行示意图 +``` + +## 非并行训练 + +{numref}`machine-learning-intro` 介绍了神经网络模型训练的过程。我们先从非并行的场景开始,这里使用 MNIST 手写数字识别案例来演示,如 {numref}`data-parallel-single` 所示,它包含了一次前向传播和一次反向传播。 + +```{figure} ../img/ch-mpi-large-model/data-parallel-single.svg +--- +width: 600px +name: data-parallel-single +--- +在单个 GPU 上进行神经网络训练 +``` + +## 数据并行 + +数据并行将数据集切分成多份,模型权重在不同 GPU 上拷贝一份。如 {numref}`data-parallel-distributed` 所示,有两块 GPU,在每块 GPU 上,有拷贝的模型权重和被切分的输入数据集;每块 GPU 上**独立**进行前向传播和反向传播,即前向传播计算每层的输出值,反向传播计算模型权重的梯度,两块 GPU 上的计算互不影响。 + + +```{figure} ../img/ch-mpi-large-model/data-parallel-distributed.svg +--- +width: 600px +name: data-parallel-distributed +--- +在两个 GPU 上进行神经网络训练 +``` + +至少在前向传播和反向传播阶段,还没有通信的开销。但到了更新模型权重部分,需要进行必要的同步,因为每块 GPU 上得到的梯度不一样,可以用求平均的方式求得梯度。这里只演示 $\boldsymbol{W}$,$\boldsymbol{\frac{\partial L}{\partial W}}^{i}$ 为单块 GPU 上的梯度,$\boldsymbol{{\frac{\partial L}{\partial W}}^{sync}}$ 为同步之后的平均值。 + +$$ +\boldsymbol{ + {\frac{\partial L}{\partial W}}^{sync} = \frac{1}{\# GPUs} \sum_{i=0}^{\# GPUs} {\frac{\partial L}{\partial W}}^{i} +} +$$ + +对这些分布在不同 GPU 上的梯度同步时,可以使用 MPI 提供的 `AllReduce` 原语。MPI 的 `AllReduce` 将每块 GPU 上分别计算得到的梯度收集起来,计算平均后,再将更新后的梯度重新分发给各块 GPU。 + +如 {numref}`data-parallel-all-reduce` 所示,梯度同步阶段,MPI 的 `AllReduce` 原语将各 GPU 上的梯度进行同步。 + +```{figure} ../img/ch-mpi-large-model/data-parallel-all-reduce.svg +--- +width: 600px +name: data-parallel-all-reduce +--- +在进行模型权重更新时,要使用 MPI 的 `AllReduce` 原语,将各 GPU 上的梯度进行同步 +``` \ No newline at end of file diff --git a/ch-mpi-large-model/large-model.md b/ch-mpi-large-model/large-model.md new file mode 100644 index 0000000..08cd79a --- /dev/null +++ b/ch-mpi-large-model/large-model.md @@ -0,0 +1,9 @@ +# 大模型 + +本章主要解释大模型的并行方法。大模型指的是神经网络的参数量很大,必须并行地进行训练和推理。大模型并行有如下特点: + +* 计算运行在 GPU 这样的加速卡上; +* 加速卡非常昂贵,应尽量提高加速卡的利用率; +* 模型参数量大,无论是训练还是推理,可能有大量数据需要在加速卡之间传输,对带宽和延迟的要求都很高。 + +本章主要从概念和原理上进行解读,具体的实现可参考其他论文和开源库。 \ No newline at end of file diff --git a/ch-mpi-large-model/nccl.md b/ch-mpi-large-model/nccl.md index 442e3eb..2a107bb 100644 --- a/ch-mpi-large-model/nccl.md +++ b/ch-mpi-large-model/nccl.md @@ -22,9 +22,9 @@ NCCL 实现了常见的通信原语 同样,其他加速器厂商也提出了自己的通信库,比如: -* AMD 提供了针对 ROCm 编程范式的 RCCL(ROCm Communication Collectives Library) +* AMD 提供了针对 ROCm 的 RCCL(ROCm Communication Collectives Library) * 华为提供了 HCCL(Huawei Collective Communication Library) 这些集合通信库都是针对特定硬件的通信库,旨在解决特定集群的通信问题。 -NCCL 主要提供了 C/C++ 编程接口,Python 社区如果使用的话,可以考虑 PyTorch 的 `torch.distributed`。NCCL 也是 PyTorch 推荐的 GPU 并行计算后端。本书不再细致讲解 `torch.distributed` 的使用,而是继续用 mpi4py 来演示大模型训练和推理过程中涉及的各类通信问题。 \ No newline at end of file +NCCL 主要提供了 C/C++ 编程接口,Python 社区如果使用的话,可以考虑 PyTorch 的 `torch.distributed`。NCCL 也是 PyTorch 推荐的 GPU 并行计算后端。本书不再细致讲解 `torch.distributed` 的使用,而是继续用 MPI 来演示大模型训练和推理过程中涉及的各类通信问题。 \ No newline at end of file diff --git a/ch-mpi-large-model/neural-network-training.md b/ch-mpi-large-model/neural-network-training.md new file mode 100644 index 0000000..fced574 --- /dev/null +++ b/ch-mpi-large-model/neural-network-training.md @@ -0,0 +1,7 @@ +(neural-network-training)= +# 模型训练 + +## 模型训练的过程 + +在深入分析大模型训练前,我们先回顾一下模型训练。 + diff --git a/ch-mpi-large-model/pipeline-parallel.md b/ch-mpi-large-model/pipeline-parallel.md index c499564..d4c6850 100644 --- a/ch-mpi-large-model/pipeline-parallel.md +++ b/ch-mpi-large-model/pipeline-parallel.md @@ -1,14 +1,54 @@ -naive pipeline是实现流水线并行训练的最直接的方法,我们将模型分为多个部分,每个部分分配一个GPU,然后进行训练,在分割模型的边界处插入通信步骤。 -我们以这个4层顺序模型为例: -Output=L4(L3(L2(L1(Input)))) +(pipeline-parallel)= +# 流水线并行 -如果把这个模型的计算分配给两个GPU,例如: -- GPU1:intermediate = L2(L1(input)) -- GPU2:output = L4(L3(intermediate)) +流水线并行是另外一种常见的大模型并行方法。数据并行将模型权重在每个 GPU 上拷贝一份,如果模型大小没有超过单块 GPU 显存大小,数据并行是最简单易用的选项。但现在的模型大到已经无法放在单块 GPU 上,比如 175B 的 GPT-3,如果用 FP16 存储,也需要 350GB 存储空间,而单块 NVIDIA A100 和 H100 为 80GB。流水线并行可以解决这个问题,它将大模型的不同层切分到不同的 GPU 上。其核心思想如 {numref}`pipeline-parallel-img` 所示。 -所以,为了完成前向传递,我们把GPU1计算出来的结果张量 intermediate 传输到GPU2,对于后向传递,我们把梯度 intermediate 从GPU2发送到GPU1。这样,模型并行训练会保持与单节点相同的输出和梯度。 +```{figure} ../img/ch-mpi-large-model/pipeline-parallel.svg +--- +width: 600px +name: pipeline-parallel-img +--- +朴素流水线并行示意图 +``` -如图,仅需要使用节点到节点通信(MPI.Send 和 MPI.Recv),并且不需要任何集体通信原语。 -![[pipeline 并行图1.png]] +## 朴素流水线并行 -数据并行和流水线并行一同作用的示意图:![[pipeline 并行图2.png]] \ No newline at end of file +假如模型被切分为两部分,第一部分在 GPU 0 上,第二部分在 GPU 1 上。每个 GPU 上计算前向传播和反向传播,如 {numref}`pipeline-parallel-distributed` 所示。 + +* 前向传播过程中,输出需要在 GPU 之间传输。 +* 反向传播过程中,损失对于输出的梯度需要在 GPU 之间传输。 + +```{figure} ../img/ch-mpi-large-model/pipeline-parallel-distributed.svg +--- +width: 600px +name: pipeline-parallel-distributed +--- +在两个 GPU 上使用流水线并行 +``` + +在这种最朴素的流水线并行场景,只需要使用点对点通信:`MPI.Send` 和 `MPI.Recv`,不需要集合通信。 + +朴素流水线并行有一个致命的缺点,那就是**GPU 利用率低**。主要体现在: + +* 任何一个时刻只有一块 GPU 在进行计算,其他 GPU 都在等待上下游的计算结果传过来。如果不考虑通信的时间成本,GPU 利用率仅为 $\frac{1}{\# GPUs}\%$。 +* 如果考虑通信的时间成本,GPU 在等待网卡的数据传输过来,GPU 计算和 GPU 之间通信没有重叠(Overlap)。GPU 设备和网络设备是相互独立的,GPU 进行当前批次计算的同时,本可以让网络设备传输上一批次的数据,两者本可以同时工作。 + +针对这些问题,研究者提出了一些方法,从数据切分和重叠的角度优化流水线并行,以提高 GPU 利用率。这些方法在朴素流水线并行基础上进行改进,感兴趣的读者可以阅读以下原文,这里不再赘述。 + +* GPipe {cite}`huang2019GPipe`。 +* PipeDream {cite}`narayanan2019PipeDream`。 +* Megatron-LM {cite}`narayanan2021Efficient`。 + +## 流水线并行 + 数据并行 + +流水线并行与数据并行是相互正交的,两者可以结合起来同时使用。由于两种并行是正交的,互不干扰,为避免数据传输错乱,应使用 MPI 的 Communicator 来做隔离。在 {numref}`mpi-hello-world` 中我们曾经提到,Communicator 可以被理解为 MPI 中的组,同一个 GPU 可以在不同的 Communicator 中。如 {numref}`pipeline-parallel-data-parallel` 所示,我们创建了两类 Communicator:红色为流水线并行的 Communicator,蓝色为数据并行的 Communicator。同一个 GPU 既属于红色,也属于蓝色:既要实现流水线并行中模型层之间的通信,也要实现数据并行的梯度的同步。 + +```{figure} ../img/ch-mpi-large-model/pipeline-parallel-data-parallel.svg +--- +width: 600px +name: pipeline-parallel-data-parallel +--- +流水线并行结合数据并行 +``` + +至此,我们介绍了两种最朴素的大模型并行训练方式:数据并行和流水线并行。工业级分布式训练库的实现比这些复杂,但背后的思想万变不离其宗。 \ No newline at end of file diff --git a/ch-ray-data/data-transform.ipynb b/ch-ray-data/data-transform.ipynb index edb1872..2babc27 100644 --- a/ch-ray-data/data-transform.ipynb +++ b/ch-ray-data/data-transform.ipynb @@ -45,23 +45,21 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/luweizheng/anaconda3/envs/dispy/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n", - "2023-12-15 12:08:18,544\tINFO util.py:159 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", - "2023-12-15 12:08:21,451\tINFO worker.py:1673 -- Started a local Ray instance.\n" + "2024-02-15 19:12:00,048\tINFO worker.py:1724 -- Started a local Ray instance.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "文件夹 /Users/luweizheng/Projects/py-101/distributed-python/ch-ray-data/../data/nyc-taxi 已存在,无需操作。\n" + "文件夹 /Users/luweizheng/Projects/godaai/distributed-python/ch-ray-data/../data/nyc-taxi 已存在,无需操作。\n" ] } ], "source": [ "import os\n", "import shutil\n", + "import warnings\n", "import urllib.request\n", "from typing import Any, Dict\n", "\n", @@ -70,6 +68,8 @@ "import torch\n", "import ray\n", "\n", + "warnings.simplefilter(action='ignore', category=FutureWarning)\n", + "\n", "if ray.is_initialized:\n", " ray.shutdown()\n", "\n", @@ -103,20 +103,52 @@ "execution_count": 2, "metadata": {}, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "128b9acfa02540eca47e5a1f91c66b4f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Parquet Files Sample 0: 0%| | 0/1 [00:00 TaskPoolMapOperator[ReadParquet] -> LimitOperator[limit=1]\n", - "2023-12-15 12:08:26,713\tINFO streaming_executor.py:105 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", - "2023-12-15 12:08:26,714\tINFO streaming_executor.py:107 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n", - "\u001b[36m(ReadParquet->SplitBlocks(200) pid=38102)\u001b[0m /Users/luweizheng/anaconda3/envs/dispy/lib/python3.11/site-packages/ray/data/_internal/arrow_block.py:128: FutureWarning: promote has been superseded by mode='default'.\n", - "\u001b[36m(ReadParquet->SplitBlocks(200) pid=38102)\u001b[0m return transform_pyarrow.concat(tables) \n", - " \r" + "2024-02-15 19:12:01,506\tINFO dataset.py:2488 -- Tip: Use `take_batch()` instead of `take() / show()` to return records in pandas or numpy batch format.\n", + "2024-02-15 19:12:01,509\tINFO set_read_parallelism.py:115 -- Using autodetected parallelism=200 for stage ReadParquet to satisfy DataContext.get_current().min_parallelism=200.\n", + "2024-02-15 19:12:01,509\tINFO set_read_parallelism.py:122 -- To satisfy the requested parallelism of 200, each read task output is split into 50 smaller blocks.\n", + "2024-02-15 19:12:01,510\tINFO streaming_executor.py:112 -- Executing DAG InputDataBuffer[Input] -> TaskPoolMapOperator[ReadParquet] -> LimitOperator[limit=1]\n", + "2024-02-15 19:12:01,510\tINFO streaming_executor.py:113 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), exclude_resources=ExecutionResources(cpu=0, gpu=0, object_store_memory=0), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", + "2024-02-15 19:12:01,510\tINFO streaming_executor.py:115 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c3e92b7a33ea4421b9e828dacf491111", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Running 0: 0%| | 0/1 [00:00SplitBlocks(50) pid=5966)\u001b[0m /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/ray/data/_internal/arrow_block.py:148: FutureWarning: promote has been superseded by mode='default'.\n", + "\u001b[36m(ReadParquet->SplitBlocks(50) pid=5966)\u001b[0m return transform_pyarrow.concat(tables)\n" ] }, { @@ -180,16 +212,27 @@ "name": "stderr", "output_type": "stream", "text": [ - "2023-12-15 12:08:28,216\tINFO split_read_output_blocks.py:101 -- Using autodetected parallelism=200 for stage ReadParquet to satisfy DataContext.get_current().min_parallelism=200.\n", - "2023-12-15 12:08:28,219\tINFO split_read_output_blocks.py:106 -- To satisfy the requested parallelism of 200, each read task output is split into 200 smaller blocks.\n", - "2023-12-15 12:08:28,221\tINFO streaming_executor.py:104 -- Executing DAG InputDataBuffer[Input] -> TaskPoolMapOperator[ReadParquet] -> TaskPoolMapOperator[Map(transform_row)] -> LimitOperator[limit=1]\n", - "2023-12-15 12:08:28,222\tINFO streaming_executor.py:105 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", - "2023-12-15 12:08:28,224\tINFO streaming_executor.py:107 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n", - "\u001b[36m(ReadParquet->SplitBlocks(200) pid=38108)\u001b[0m /Users/luweizheng/anaconda3/envs/dispy/lib/python3.11/site-packages/ray/data/_internal/arrow_block.py:128: FutureWarning: promote has been superseded by mode='default'.\n", - "\u001b[36m(ReadParquet->SplitBlocks(200) pid=38108)\u001b[0m return transform_pyarrow.concat(tables) \n", - " \r" + "2024-02-15 19:12:02,068\tINFO set_read_parallelism.py:115 -- Using autodetected parallelism=200 for stage ReadParquet to satisfy DataContext.get_current().min_parallelism=200.\n", + "2024-02-15 19:12:02,069\tINFO set_read_parallelism.py:122 -- To satisfy the requested parallelism of 200, each read task output is split into 50 smaller blocks.\n", + "2024-02-15 19:12:02,069\tINFO streaming_executor.py:112 -- Executing DAG InputDataBuffer[Input] -> TaskPoolMapOperator[ReadParquet] -> TaskPoolMapOperator[Map(transform_row)] -> LimitOperator[limit=1]\n", + "2024-02-15 19:12:02,069\tINFO streaming_executor.py:113 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), exclude_resources=ExecutionResources(cpu=0, gpu=0, object_store_memory=0), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", + "2024-02-15 19:12:02,070\tINFO streaming_executor.py:115 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n" ] }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "28fbc3a6677f4b1b809b0662a977f691", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Running 0: 0%| | 0/1 [00:00 TaskPoolMapOperator[ReadParquet] -> TaskPoolMapOperator[MapBatches(transform_df)] -> LimitOperator[limit=10]\n", - "2023-12-15 12:08:35,842\tINFO streaming_executor.py:105 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", - "2023-12-15 12:08:35,843\tINFO streaming_executor.py:107 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n", - "\u001b[36m(ReadParquet->SplitBlocks(200) pid=38103)\u001b[0m /Users/luweizheng/anaconda3/envs/dispy/lib/python3.11/site-packages/ray/data/_internal/arrow_block.py:128: FutureWarning: promote has been superseded by mode='default'.\n", - "\u001b[36m(ReadParquet->SplitBlocks(200) pid=38103)\u001b[0m return transform_pyarrow.concat(tables) \n", - " \r" + "2024-02-15 19:12:04,789\tINFO set_read_parallelism.py:115 -- Using autodetected parallelism=200 for stage ReadParquet to satisfy DataContext.get_current().min_parallelism=200.\n", + "2024-02-15 19:12:04,790\tINFO set_read_parallelism.py:122 -- To satisfy the requested parallelism of 200, each read task output is split into 50 smaller blocks.\n", + "2024-02-15 19:12:04,790\tINFO streaming_executor.py:112 -- Executing DAG InputDataBuffer[Input] -> TaskPoolMapOperator[ReadParquet] -> TaskPoolMapOperator[MapBatches(transform_df)] -> LimitOperator[limit=10]\n", + "2024-02-15 19:12:04,790\tINFO streaming_executor.py:113 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), exclude_resources=ExecutionResources(cpu=0, gpu=0, object_store_memory=0), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", + "2024-02-15 19:12:04,790\tINFO streaming_executor.py:115 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3af6e0ef7f5643a2b57cc3fcb0292e1c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Running 0: 0%| | 0/1 [00:00SplitBlocks(50) pid=5960)\u001b[0m /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/ray/data/_internal/arrow_block.py:148: FutureWarning: promote has been superseded by mode='default'.\n", + "\u001b[36m(ReadParquet->SplitBlocks(50) pid=5960)\u001b[0m return transform_pyarrow.concat(tables)\n" ] }, { @@ -289,16 +351,27 @@ "name": "stderr", "output_type": "stream", "text": [ - "2023-12-15 12:08:37,202\tINFO split_read_output_blocks.py:101 -- Using autodetected parallelism=200 for stage ReadParquet to satisfy DataContext.get_current().min_parallelism=200.\n", - "2023-12-15 12:08:37,203\tINFO split_read_output_blocks.py:106 -- To satisfy the requested parallelism of 200, each read task output is split into 200 smaller blocks.\n", - "2023-12-15 12:08:37,205\tINFO streaming_executor.py:104 -- Executing DAG InputDataBuffer[Input] -> TaskPoolMapOperator[ReadParquet] -> TaskPoolMapOperator[MapBatches()]\n", - "2023-12-15 12:08:37,207\tINFO streaming_executor.py:105 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", - "2023-12-15 12:08:37,208\tINFO streaming_executor.py:107 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n", - "\u001b[36m(ReadParquet->SplitBlocks(200) pid=38106)\u001b[0m /Users/luweizheng/anaconda3/envs/dispy/lib/python3.11/site-packages/ray/data/_internal/arrow_block.py:128: FutureWarning: promote has been superseded by mode='default'.\n", - "\u001b[36m(ReadParquet->SplitBlocks(200) pid=38106)\u001b[0m return transform_pyarrow.concat(tables) \n", - " \r" + "2024-02-15 19:12:05,225\tINFO set_read_parallelism.py:115 -- Using autodetected parallelism=200 for stage ReadParquet to satisfy DataContext.get_current().min_parallelism=200.\n", + "2024-02-15 19:12:05,225\tINFO set_read_parallelism.py:122 -- To satisfy the requested parallelism of 200, each read task output is split into 50 smaller blocks.\n", + "2024-02-15 19:12:05,225\tINFO streaming_executor.py:112 -- Executing DAG InputDataBuffer[Input] -> TaskPoolMapOperator[ReadParquet] -> TaskPoolMapOperator[MapBatches()]\n", + "2024-02-15 19:12:05,225\tINFO streaming_executor.py:113 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), exclude_resources=ExecutionResources(cpu=0, gpu=0, object_store_memory=0), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", + "2024-02-15 19:12:05,226\tINFO streaming_executor.py:115 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n" ] }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b77916ceb4b74390badf1461ce5fca01", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Running 0: 0%| | 0/1 [00:00 TaskPoolMapOperator[ReadParquet] -> TaskPoolMapOperator[MapBatches(transform_df)] -> LimitOperator[limit=100] -> ActorPoolMapOperator[MapBatches(TorchPredictor)] -> LimitOperator[limit=3]\n", - "2023-12-15 12:08:44,230\tINFO streaming_executor.py:105 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", - "2023-12-15 12:08:44,230\tINFO streaming_executor.py:107 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n", - "2023-12-15 12:08:44,276\tINFO actor_pool_map_operator.py:114 -- MapBatches(TorchPredictor): Waiting for 2 pool actors to start...\n", - "\u001b[36m(ReadParquet->SplitBlocks(200) pid=38103)\u001b[0m /Users/luweizheng/anaconda3/envs/dispy/lib/python3.11/site-packages/ray/data/_internal/arrow_block.py:128: FutureWarning: promote has been superseded by mode='default'.\n", - "\u001b[36m(ReadParquet->SplitBlocks(200) pid=38103)\u001b[0m return transform_pyarrow.concat(tables) \n", - "2023-12-15 12:08:49,380\tWARNING actor_pool_map_operator.py:271 -- To ensure full parallelization across an actor pool of size 2, the Dataset should consist of at least 2 distinct blocks. Consider increasing the parallelism when creating the Dataset.\n" + "2024-02-15 19:12:06,821\tWARNING util.py:546 -- The argument ``compute`` is deprecated in Ray 2.9. Please specify argument ``concurrency`` instead. For more information, see https://docs.ray.io/en/master/data/transforming-data.html#stateful-transforms.\n", + "2024-02-15 19:12:06,829\tINFO set_read_parallelism.py:115 -- Using autodetected parallelism=200 for stage ReadParquet to satisfy DataContext.get_current().min_parallelism=200.\n", + "2024-02-15 19:12:06,830\tINFO set_read_parallelism.py:122 -- To satisfy the requested parallelism of 200, each read task output is split into 50 smaller blocks.\n", + "2024-02-15 19:12:06,830\tINFO streaming_executor.py:112 -- Executing DAG InputDataBuffer[Input] -> TaskPoolMapOperator[ReadParquet] -> TaskPoolMapOperator[MapBatches(transform_df)] -> LimitOperator[limit=100] -> ActorPoolMapOperator[MapBatches(TorchPredictor)] -> LimitOperator[limit=3]\n", + "2024-02-15 19:12:06,831\tINFO streaming_executor.py:113 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), exclude_resources=ExecutionResources(cpu=0, gpu=0, object_store_memory=0), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", + "2024-02-15 19:12:06,832\tINFO streaming_executor.py:115 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n", + "2024-02-15 19:12:06,846\tINFO actor_pool_map_operator.py:114 -- MapBatches(TorchPredictor): Waiting for 2 pool actors to start...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "95bb2562908a46ec97ef831ae5ad6072", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Running 0: 0%| | 0/1 [00:00SplitBlocks(50) pid=5964)\u001b[0m /Users/luweizheng/miniconda3/envs/dispy/lib/python3.11/site-packages/ray/data/_internal/arrow_block.py:148: FutureWarning: promote has been superseded by mode='default'.\n", + "\u001b[36m(ReadParquet->SplitBlocks(50) pid=5964)\u001b[0m return transform_pyarrow.concat(tables)\n", + "2024-02-15 19:12:08,283\tWARNING actor_pool_map_operator.py:278 -- To ensure full parallelization across an actor pool of size 2, the Dataset should consist of at least 2 distinct blocks. Consider increasing the parallelism when creating the Dataset.\n" ] }, { @@ -398,29 +492,81 @@ "name": "stderr", "output_type": "stream", "text": [ - "2023-12-15 12:08:49,415\tINFO streaming_executor.py:104 -- Executing DAG InputDataBuffer[Input] -> AllToAllOperator[Aggregate] -> LimitOperator[limit=20]\n", - "2023-12-15 12:08:49,415\tINFO streaming_executor.py:105 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", - "2023-12-15 12:08:49,417\tINFO streaming_executor.py:107 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n", - "\n", - "\u001b[A\n", - "\u001b[A\n", - "\n", - "\u001b[A\u001b[A\n", - "\n", - "Sort Sample 0: 0%| | 0/4 [00:00 AllToAllOperator[Aggregate] -> LimitOperator[limit=20]\n", + "2024-02-15 19:12:08,333\tINFO streaming_executor.py:113 -- Execution config: ExecutionOptions(resource_limits=ExecutionResources(cpu=None, gpu=None, object_store_memory=None), exclude_resources=ExecutionResources(cpu=0, gpu=0, object_store_memory=0), locality_with_output=False, preserve_order=False, actor_locality_enabled=True, verbose_progress=False)\n", + "2024-02-15 19:12:08,333\tINFO streaming_executor.py:115 -- Tip: For detailed progress reporting, run `ray.data.DataContext.get_current().execution_options.verbose_progress = True`\n" ] }, { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/luweizheng/anaconda3/envs/dispy/lib/python3.11/site-packages/ray/data/_internal/arrow_block.py:128: FutureWarning: promote has been superseded by mode='default'.\n", - " return transform_pyarrow.concat(tables)\n", - " \n", - "\u001b[A\n", - "\n", - "\u001b[A\u001b[A" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "54684a32e65142cda779ee0745f7b35e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "- Aggregate 1: 0%| | 0/4 [00:00 + + + + + + + + + + + + + + + + + + + + + diff --git a/drawio/ch-data-science/model-training-input-output.drawio b/drawio/ch-data-science/model-training-input-output.drawio new file mode 100644 index 0000000..e5f945b --- /dev/null +++ b/drawio/ch-data-science/model-training-input-output.drawio @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drawio/ch-data-science/model-training.drawio b/drawio/ch-data-science/model-training.drawio new file mode 100644 index 0000000..c9d1618 --- /dev/null +++ b/drawio/ch-data-science/model-training.drawio @@ -0,0 +1,298 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drawio/ch-mpi-large-model/data-parallel-all-reduce.drawio b/drawio/ch-mpi-large-model/data-parallel-all-reduce.drawio new file mode 100644 index 0000000..2367a6f --- /dev/null +++ b/drawio/ch-mpi-large-model/data-parallel-all-reduce.drawio @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drawio/ch-mpi-large-model/data-parallel-distributed.drawio b/drawio/ch-mpi-large-model/data-parallel-distributed.drawio new file mode 100644 index 0000000..6a8904a --- /dev/null +++ b/drawio/ch-mpi-large-model/data-parallel-distributed.drawio @@ -0,0 +1,328 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drawio/ch-mpi-large-model/data-parallel-single.drawio b/drawio/ch-mpi-large-model/data-parallel-single.drawio new file mode 100644 index 0000000..2741c7f --- /dev/null +++ b/drawio/ch-mpi-large-model/data-parallel-single.drawio @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drawio/ch-mpi-large-model/data-parallel.drawio b/drawio/ch-mpi-large-model/data-parallel.drawio new file mode 100644 index 0000000..8d88f2b --- /dev/null +++ b/drawio/ch-mpi-large-model/data-parallel.drawio @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drawio/ch-mpi-large-model/pipeline-parallel-data-parallel.drawio b/drawio/ch-mpi-large-model/pipeline-parallel-data-parallel.drawio new file mode 100644 index 0000000..3289139 --- /dev/null +++ b/drawio/ch-mpi-large-model/pipeline-parallel-data-parallel.drawio @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drawio/ch-mpi-large-model/pipeline-parallel-distributed.drawio b/drawio/ch-mpi-large-model/pipeline-parallel-distributed.drawio new file mode 100644 index 0000000..6c0f67d --- /dev/null +++ b/drawio/ch-mpi-large-model/pipeline-parallel-distributed.drawio @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drawio/ch-mpi-large-model/pipeline-parallel.drawio b/drawio/ch-mpi-large-model/pipeline-parallel.drawio new file mode 100644 index 0000000..35bf846 --- /dev/null +++ b/drawio/ch-mpi-large-model/pipeline-parallel.drawio @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/ch-dask-dataframe/dataframe-model.svg b/img/ch-dask-dataframe/dataframe-model.svg new file mode 100644 index 0000000..6f737a9 --- /dev/null +++ b/img/ch-dask-dataframe/dataframe-model.svg @@ -0,0 +1,4 @@ + + + +
数据
数据
行标签
行标签
列标签
列标签
Text is not SVG - cannot display
\ No newline at end of file diff --git a/img/ch-dask-dataframe/divisions.png b/img/ch-dask-dataframe/divisions.png new file mode 100644 index 0000000000000000000000000000000000000000..ef58e5950f875159722dae62a182373a75a25238 GIT binary patch literal 236730 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 Mpg*7Z){Vp4$#|1}=48^_UGh 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