From 4f9c228d641eb2cfe42fda0229cbff79df34e05f Mon Sep 17 00:00:00 2001 From: Lu Weizheng Date: Tue, 2 Apr 2024 14:42:15 +0800 Subject: [PATCH] ray cluster --- _static/logo.ico | Bin 0 -> 4022 bytes _toc.yml | 6 +- ch-dask-dataframe/shuffle-test.ipynb | 162 +++++++++-------- ch-ray-cluster/gpu.py | 20 ++ ch-ray-cluster/index.md | 4 + ch-ray-cluster/pg.py | 33 ++++ ch-ray-cluster/ray-cluster.md | 77 ++++++++ ch-ray-cluster/ray-job.md | 171 ++++++++++++++++++ ch-ray-cluster/ray-resource.md | 137 ++++++++++++++ ch-ray-cluster/script.py | 23 +++ ch-ray-cluster/sdk.py | 23 +++ ch-ray-core/ray-internal.md | 34 ---- conf.py | 2 +- .../ray-cluster.drawio | 25 +-- img/ch-ray-cluster/pg-pack.png | Bin 0 -> 40047 bytes img/ch-ray-cluster/pg-spread.png | Bin 0 -> 38558 bytes img/ch-ray-cluster/ray-cluster.svg | 4 + img/ch-ray-core/ray-cluster.svg | 4 - 18 files changed, 599 insertions(+), 126 deletions(-) create mode 100644 _static/logo.ico create mode 100644 ch-ray-cluster/gpu.py create mode 100644 ch-ray-cluster/index.md create mode 100644 ch-ray-cluster/pg.py create mode 100644 ch-ray-cluster/ray-cluster.md create mode 100644 ch-ray-cluster/ray-job.md create mode 100644 ch-ray-cluster/ray-resource.md create mode 100644 ch-ray-cluster/script.py create mode 100644 ch-ray-cluster/sdk.py delete mode 100644 ch-ray-core/ray-internal.md rename drawio/{ch-ray-core => ch-ray-cluster}/ray-cluster.drawio (84%) create mode 100644 img/ch-ray-cluster/pg-pack.png create mode 100644 img/ch-ray-cluster/pg-spread.png create mode 100644 img/ch-ray-cluster/ray-cluster.svg delete mode 100644 img/ch-ray-core/ray-cluster.svg diff --git a/_static/logo.ico b/_static/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..5169b6d6eed8f1609547423dd546f91ae9cf28c9 GIT binary patch literal 4022 zcmeHKYjjlA72Y#>OcFvKK%SL{4_a-bSfx@Bi;p}>cq9Z3VnCju#E^s}BtS?Y1VRXe z5dlHo1wlXym`EsTE868+uC}!Lqjuq1UA3jP{?JuKQ|`UzvHRV5b&`oo`k$Ni%{lkX zoc+z-=lk}Z5IOKSW)#`4bR?cAnTYNM#sHH6d);&4y}KXq!n<($)n5V5>Lwrx$O6hZ z`^^cQcP4^!-|&BA&{0)ok*dsd%9W3gmvs}%WWx`$<=XE@$u${vJR6BXF|Y`@2k@*1 z2lK5tFxCTK1I#%;|D5w*-LGfte^}3Qhv~xI!CO3YRh2`s`bkpNnbT#>luzZVM_8`8 zzfZ0kyW02{IXJT?7O{>3(i1bQ`vZ&_K)+b_J&+1~1TbfxXU;ptJnb;+!abkq;%zVL zl8qUdRdS&e-mJJWGZ2TU}=FwMWha-ibx;P2(Zg^taalKfF zv0n{*_cs5ZIjf5~_jTriW2|Sq!pNQV(`~GuZMvrC)wZbxDS_9{KS$O) z_O+}ZkKBL2vguBi8`7>?os`U(7xNI)0{qiJ5o7A^gYDtm67DxWo4R%eBy?uTXFk6ErB$EZ8Y^>?t?nDjEIvdxhX z+!}w*S>o(-xtw>pj|+~np1GHG@ebC{Y+^mPk@dV9RtuN2TC#xEPoL4MV*2l7^;5;N z_R(vy{vnoY??c_kuxv^BQnU_xTrH@$Wq$iW!3&&8;QZt7>FEbp7wtsd+mZWv#$(Xri8g-Q=;d5Z?*n@LE9b! zcLp@~sZVpLdmXCZA zvBPOuyB*!Kwy2W}j$532#_MO>(|M=d0M7c9pEfY+r?(K!n{-J_F*IieG`IA5Xs+Z6 z&OHfvlUy|+M>afk!>>8fl8oHr{)*f;h)%~%u``^lmm}HQ8z;r)iTLg{@G6X#mE z%k8c6t^jZFM9*DI;7-w{t3K$jxgtZY$S>s5Mw0apA3&cA(p(~owwPn6d$(|hv9&vb zt$k5!9g1P=SPYv#OJcG8QRucDdTw^>dEl7S984RS>(V+BWu*kKw?cFH?(@8wTT#H! z+!37lIMn?g-acn*ZUpKcdtGdeScbZPBwn&}cd>OKnyn+TY`q%C=II1B&nB{U5<1y1 z&29YNN$6t%&VduPsEoipK`mMM1vJMV%~fTytog~`;PvCsoc8oNkLKc8Y>Da-Tb=RZ z#jx?#?#P4ID^Xuqhle2dp=_RvXY=)8Y@Qpz=3B{Zo*(7@&s8GP{=`7qz&&~lsl`j8 z;q@+6UTk}v(Q6rg({l9#BGBh-&9y}f(H=fuv`3P)&qdZ=SA=yi`Z4Qh>G3=~)KIw*eA;mI&k@hR2_d9nPz3BrfL+(|@ zGy?Z%kJndD;m|$@&24cpc$+Yi=;$A!@89Fe>`EcCCpFsaPMK)*q~;jE9y!A3PNu%U zKhpohgWRdg9O9Y#?PuIhSr`0l^=Ync2)M)kDYj7tJRcGPTJ(G;(doPw?>7N2!5_2X zk0{~x)+%IE(1KYziST=d@LYcqn*S>BZ1rodJ(3agpm0+d;s)*XPT>Jt3-`c_SFBy| zqZc==J&`M|83e6Fh3px)0H34R{3uSYzW?n3nq$%7{0h8THn*K?@Z~xGLooyoR9T1M z1KSG$#C!-}CIP`)2A!kF5PU|m@w=7wvo)Z(t>Ne=j!WQ;MSVl|+Whc5L4S?rDR{p> zJ~m&APX|Kw4Yq*aQUva?_)YV%X!U7so6`VqDS9n^(^*3EWS$=}$?Qt{%=TUYZzkxJfii`?tM8#A!(!JDCbGP{$V@Olya)@%GS{R+Il z9X%ioJ|y$)Z}xw19D9!bMxrGZJu5N>=N>0I$!VYaw_V2jchb#2-ABf|BV&wrM#dYz z85L%{i+_}EU$-mZs{s41Pk3Yek}WYQ>5!Ukq1T;xok+>d-0%4 e(`2AIo~hZz)CA}-UJq;l-q9|;#yk8x{_($}!jTyO literal 0 HcmV?d00001 diff --git a/_toc.yml b/_toc.yml index d6fb845..6f141bc 100644 --- a/_toc.yml +++ b/_toc.yml @@ -33,7 +33,11 @@ subtrees: - file: ch-ray-core/remote-function - file: ch-ray-core/remote-object - file: ch-ray-core/remote-class - - file: ch-ray-core/ray-internal + - file: ch-ray-cluster/index + entries: + - file: ch-ray-cluster/ray-cluster + - file: ch-ray-cluster/ray-job + - file: ch-ray-cluster/ray-resource - file: ch-ray-data/index entries: - file: ch-ray-data/ray-data-intro diff --git a/ch-dask-dataframe/shuffle-test.ipynb b/ch-dask-dataframe/shuffle-test.ipynb index 63052e0..1b8d22f 100644 --- a/ch-dask-dataframe/shuffle-test.ipynb +++ b/ch-dask-dataframe/shuffle-test.ipynb @@ -57,7 +57,37 @@ "cell_type": "code", "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_6760/928681557.py:1: DeprecationWarning: The current Dask DataFrame implementation is deprecated. \n", + "In a future release, Dask DataFrame will use new implementation that\n", + "contains several improvements including a logical query planning.\n", + "The user-facing DataFrame API will remain unchanged.\n", + "\n", + "The new implementation is already available and can be enabled by\n", + "installing the dask-expr library:\n", + "\n", + " $ pip install dask-expr\n", + "\n", + "and turning the query planning option on:\n", + "\n", + " >>> import dask\n", + " >>> dask.config.set({'dataframe.query-planning': True})\n", + " >>> import dask.dataframe as dd\n", + "\n", + "API documentation for the new implementation is available at\n", + "https://docs.dask.org/en/stable/dask-expr-api.html\n", + "\n", + "Any feedback can be reported on the Dask issue tracker\n", + "https://github.com/dask/dask/issues \n", + "\n", + " import dask.dataframe as dd\n" + ] + } + ], "source": [ "import dask.dataframe as dd\n", "import pandas as pd\n", @@ -794,13 +824,6 @@ "na_rows.head(3)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "code", "execution_count": 5, @@ -1171,7 +1194,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -1237,7 +1260,7 @@ "3 B 400 0.4" ] }, - "execution_count": 12, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -1363,21 +1386,52 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 12, "metadata": {}, "outputs": [ { - "ename": "KeyError", - "evalue": "\"Columns not found: 'EmployerSize'\"", + "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------------------------\nKeyError('EmployerSize')\n\nTraceback:\n---------\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/dask/dataframe/utils.py\", line 194, in raise_on_meta_error\n yield\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/dask/dataframe/core.py\", line 7174, in _emulate\n return func(*_extract_meta(args, True), **_extract_meta(kwargs, True))\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py\", line 1190, in _groupby_apply_funcs\n r = func(grouped, **func_kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py\", line 1266, in _apply_func_to_column\n return func(df_like[column])\n ^^^^^^^^^^^^^^^^^^^^^\n File \"/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_6760/1210873169.py\", line 4, in chunk\n return (chunk.apply(weighted_func), chunk.sum()[\"EmployerSize\"])\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/generic.py\", line 230, in apply\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/groupby.py\", line 1824, in apply\n Series or DataFrame\n ^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/groupby.py\", line 1885, in _python_apply_general\n \"\"\"\n \n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/ops.py\", line 919, in apply_groupwise\n File \"/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_6760/1210873169.py\", line 3, in weighted_func\n return (df[\"EmployerSize\"] * df[\"DiffMeanHourlyPercent\"]).sum()\n ~~^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/series.py\", line 1112, in __getitem__\n ) from err\n ^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/series.py\", line 1228, in _get_value\n return getattr(self, \"_cacher\", None) is not None\n ^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/indexes/base.py\", line 3812, in get_loc\n", "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'\"" + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/indexes/base.py:3805\u001b[0m, in \u001b[0;36mget_loc\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 3804\u001b[0m \u001b[38;5;66;03m# error: \"IndexEngine\" has no attribute \"_extract_level_codes\"\u001b[39;00m\n\u001b[0;32m-> 3805\u001b[0m tgt_values \u001b[38;5;241m=\u001b[39m engine\u001b[38;5;241m.\u001b[39m_extract_level_codes( \u001b[38;5;66;03m# type: ignore[attr-defined]\u001b[39;00m\n\u001b[1;32m 3806\u001b[0m target\n\u001b[1;32m 3807\u001b[0m )\n\u001b[1;32m 3809\u001b[0m indexer \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_engine\u001b[38;5;241m.\u001b[39mget_indexer(tgt_values)\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)", + "File \u001b[0;32m~/miniconda3/envs/dask/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/dask/lib/python3.11/site-packages/dask/dataframe/core.py:7174\u001b[0m, in \u001b[0;36m_emulate\u001b[0;34m(func, udf, *args, **kwargs)\u001b[0m\n\u001b[1;32m 7173\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-> 7174\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/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py:1190\u001b[0m, in \u001b[0;36m_groupby_apply_funcs\u001b[0;34m(df, *by, **kwargs)\u001b[0m\n\u001b[1;32m 1189\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-> 1190\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 1192\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/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py:1266\u001b[0m, in \u001b[0;36m_apply_func_to_column\u001b[0;34m(df_like, column, func)\u001b[0m\n\u001b[1;32m 1264\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m func(df_like)\n\u001b[0;32m-> 1266\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[12], line 4\u001b[0m, in \u001b[0;36mchunk\u001b[0;34m(chunk)\u001b[0m\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;241m.\u001b[39msum()[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEmployerSize\u001b[39m\u001b[38;5;124m\"\u001b[39m])\n", + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/generic.py:230\u001b[0m, in \u001b[0;36mapply\u001b[0;34m(self, func, *args, **kwargs)\u001b[0m\n\u001b[1;32m 186\u001b[0m \u001b[38;5;28;01myield\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_selected_obj\n\u001b[1;32m 188\u001b[0m _agg_examples_doc \u001b[38;5;241m=\u001b[39m dedent(\n\u001b[1;32m 189\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 190\u001b[0m \u001b[38;5;124;03mExamples\u001b[39;00m\n\u001b[1;32m 191\u001b[0m \u001b[38;5;124;03m--------\u001b[39;00m\n\u001b[1;32m 192\u001b[0m \u001b[38;5;124;03m>>> s = pd.Series([1, 2, 3, 4])\u001b[39;00m\n\u001b[1;32m 193\u001b[0m \n\u001b[1;32m 194\u001b[0m \u001b[38;5;124;03m>>> s\u001b[39;00m\n\u001b[1;32m 195\u001b[0m \u001b[38;5;124;03m0 1\u001b[39;00m\n\u001b[1;32m 196\u001b[0m \u001b[38;5;124;03m1 2\u001b[39;00m\n\u001b[1;32m 197\u001b[0m \u001b[38;5;124;03m2 3\u001b[39;00m\n\u001b[1;32m 198\u001b[0m \u001b[38;5;124;03m3 4\u001b[39;00m\n\u001b[1;32m 199\u001b[0m \u001b[38;5;124;03mdtype: int64\u001b[39;00m\n\u001b[1;32m 200\u001b[0m \n\u001b[1;32m 201\u001b[0m \u001b[38;5;124;03m>>> s.groupby([1, 1, 2, 2]).min()\u001b[39;00m\n\u001b[1;32m 202\u001b[0m \u001b[38;5;124;03m1 1\u001b[39;00m\n\u001b[1;32m 203\u001b[0m \u001b[38;5;124;03m2 3\u001b[39;00m\n\u001b[1;32m 204\u001b[0m \u001b[38;5;124;03mdtype: int64\u001b[39;00m\n\u001b[1;32m 205\u001b[0m \n\u001b[1;32m 206\u001b[0m \u001b[38;5;124;03m>>> s.groupby([1, 1, 2, 2]).agg('min')\u001b[39;00m\n\u001b[1;32m 207\u001b[0m \u001b[38;5;124;03m1 1\u001b[39;00m\n\u001b[1;32m 208\u001b[0m \u001b[38;5;124;03m2 3\u001b[39;00m\n\u001b[1;32m 209\u001b[0m \u001b[38;5;124;03mdtype: int64\u001b[39;00m\n\u001b[1;32m 210\u001b[0m \n\u001b[1;32m 211\u001b[0m \u001b[38;5;124;03m>>> s.groupby([1, 1, 2, 2]).agg(['min', 'max'])\u001b[39;00m\n\u001b[1;32m 212\u001b[0m \u001b[38;5;124;03m min max\u001b[39;00m\n\u001b[1;32m 213\u001b[0m \u001b[38;5;124;03m1 1 2\u001b[39;00m\n\u001b[1;32m 214\u001b[0m \u001b[38;5;124;03m2 3 4\u001b[39;00m\n\u001b[1;32m 215\u001b[0m \n\u001b[1;32m 216\u001b[0m \u001b[38;5;124;03mThe output column names can be controlled by passing\u001b[39;00m\n\u001b[1;32m 217\u001b[0m \u001b[38;5;124;03mthe desired column names and aggregations as keyword arguments.\u001b[39;00m\n\u001b[1;32m 218\u001b[0m \n\u001b[1;32m 219\u001b[0m \u001b[38;5;124;03m>>> s.groupby([1, 1, 2, 2]).agg(\u001b[39;00m\n\u001b[1;32m 220\u001b[0m \u001b[38;5;124;03m... minimum='min',\u001b[39;00m\n\u001b[1;32m 221\u001b[0m \u001b[38;5;124;03m... maximum='max',\u001b[39;00m\n\u001b[1;32m 222\u001b[0m \u001b[38;5;124;03m... )\u001b[39;00m\n\u001b[1;32m 223\u001b[0m \u001b[38;5;124;03m minimum maximum\u001b[39;00m\n\u001b[1;32m 224\u001b[0m \u001b[38;5;124;03m1 1 2\u001b[39;00m\n\u001b[1;32m 225\u001b[0m \u001b[38;5;124;03m2 3 4\u001b[39;00m\n\u001b[1;32m 226\u001b[0m \n\u001b[1;32m 227\u001b[0m \u001b[38;5;124;03m.. versionchanged:: 1.3.0\u001b[39;00m\n\u001b[1;32m 228\u001b[0m \n\u001b[1;32m 229\u001b[0m \u001b[38;5;124;03m The resulting dtype will reflect the return value of the aggregating function.\u001b[39;00m\n\u001b[0;32m--> 230\u001b[0m \n\u001b[1;32m 231\u001b[0m \u001b[38;5;124;03m>>> s.groupby([1, 1, 2, 2]).agg(lambda x: x.astype(float).min())\u001b[39;00m\n\u001b[1;32m 232\u001b[0m \u001b[38;5;124;03m1 1.0\u001b[39;00m\n\u001b[1;32m 233\u001b[0m \u001b[38;5;124;03m2 3.0\u001b[39;00m\n\u001b[1;32m 234\u001b[0m \u001b[38;5;124;03mdtype: float64\u001b[39;00m\n\u001b[1;32m 235\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 236\u001b[0m )\n\u001b[1;32m 238\u001b[0m \u001b[38;5;129m@Appender\u001b[39m(\n\u001b[1;32m 239\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 240\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 241\u001b[0m )\n\u001b[1;32m 242\u001b[0m )\n\u001b[1;32m 243\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):\n", + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/groupby.py:1824\u001b[0m, in \u001b[0;36mapply\u001b[0;34m(self, func, include_groups, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1810\u001b[0m \u001b[38;5;129m@final\u001b[39m\n\u001b[1;32m 1811\u001b[0m \u001b[38;5;129m@Substitution\u001b[39m(name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mgroupby\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 1812\u001b[0m \u001b[38;5;129m@Appender\u001b[39m(_common_see_also)\n\u001b[1;32m 1813\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mall\u001b[39m(\u001b[38;5;28mself\u001b[39m, skipna: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m):\n\u001b[1;32m 1814\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1815\u001b[0m \u001b[38;5;124;03m Return True if all values in the group are truthful, else False.\u001b[39;00m\n\u001b[1;32m 1816\u001b[0m \n\u001b[1;32m 1817\u001b[0m \u001b[38;5;124;03m Parameters\u001b[39;00m\n\u001b[1;32m 1818\u001b[0m \u001b[38;5;124;03m ----------\u001b[39;00m\n\u001b[1;32m 1819\u001b[0m \u001b[38;5;124;03m skipna : bool, default True\u001b[39;00m\n\u001b[1;32m 1820\u001b[0m \u001b[38;5;124;03m Flag to ignore nan values during truth testing.\u001b[39;00m\n\u001b[1;32m 1821\u001b[0m \n\u001b[1;32m 1822\u001b[0m \u001b[38;5;124;03m Returns\u001b[39;00m\n\u001b[1;32m 1823\u001b[0m \u001b[38;5;124;03m -------\u001b[39;00m\n\u001b[0;32m-> 1824\u001b[0m \u001b[38;5;124;03m Series or DataFrame\u001b[39;00m\n\u001b[1;32m 1825\u001b[0m \u001b[38;5;124;03m DataFrame or Series of boolean values, where a value is True if all elements\u001b[39;00m\n\u001b[1;32m 1826\u001b[0m \u001b[38;5;124;03m are True within its respective group, False otherwise.\u001b[39;00m\n\u001b[1;32m 1827\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m 1828\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_bool_agg(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mall\u001b[39m\u001b[38;5;124m\"\u001b[39m, skipna)\n", + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/groupby.py:1885\u001b[0m, in \u001b[0;36m_python_apply_general\u001b[0;34m(self, f, data, not_indexed_same, is_transform, is_agg)\u001b[0m\n\u001b[1;32m 1876\u001b[0m \u001b[38;5;129m@final\u001b[39m\n\u001b[1;32m 1877\u001b[0m \u001b[38;5;129m@Substitution\u001b[39m(name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mgroupby\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 1878\u001b[0m \u001b[38;5;129m@Substitution\u001b[39m(see_also\u001b[38;5;241m=\u001b[39m_common_see_also)\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1883\u001b[0m engine_kwargs: \u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, \u001b[38;5;28mbool\u001b[39m] \u001b[38;5;241m|\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 1884\u001b[0m ):\n\u001b[0;32m-> 1885\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1886\u001b[0m \u001b[38;5;124;03m Compute mean of groups, excluding missing values.\u001b[39;00m\n\u001b[1;32m 1887\u001b[0m \n\u001b[1;32m 1888\u001b[0m \u001b[38;5;124;03m Parameters\u001b[39;00m\n\u001b[1;32m 1889\u001b[0m \u001b[38;5;124;03m ----------\u001b[39;00m\n\u001b[1;32m 1890\u001b[0m \u001b[38;5;124;03m numeric_only : bool, default True\u001b[39;00m\n\u001b[1;32m 1891\u001b[0m \u001b[38;5;124;03m Include only float, int, boolean columns. If None, will attempt to use\u001b[39;00m\n\u001b[1;32m 1892\u001b[0m \u001b[38;5;124;03m everything, then use only numeric data.\u001b[39;00m\n\u001b[1;32m 1893\u001b[0m \n\u001b[1;32m 1894\u001b[0m \u001b[38;5;124;03m engine : str, default None\u001b[39;00m\n\u001b[1;32m 1895\u001b[0m \u001b[38;5;124;03m * ``'cython'`` : Runs the operation through C-extensions from cython.\u001b[39;00m\n\u001b[1;32m 1896\u001b[0m \u001b[38;5;124;03m * ``'numba'`` : Runs the operation through JIT compiled code from numba.\u001b[39;00m\n\u001b[1;32m 1897\u001b[0m \u001b[38;5;124;03m * ``None`` : Defaults to ``'cython'`` or globally setting\u001b[39;00m\n\u001b[1;32m 1898\u001b[0m \u001b[38;5;124;03m ``compute.use_numba``\u001b[39;00m\n\u001b[1;32m 1899\u001b[0m \n\u001b[1;32m 1900\u001b[0m \u001b[38;5;124;03m .. versionadded:: 1.4.0\u001b[39;00m\n\u001b[1;32m 1901\u001b[0m \n\u001b[1;32m 1902\u001b[0m \u001b[38;5;124;03m engine_kwargs : dict, default None\u001b[39;00m\n\u001b[1;32m 1903\u001b[0m \u001b[38;5;124;03m * For ``'cython'`` engine, there are no accepted ``engine_kwargs``\u001b[39;00m\n\u001b[1;32m 1904\u001b[0m \u001b[38;5;124;03m * For ``'numba'`` engine, the engine can accept ``nopython``, ``nogil``\u001b[39;00m\n\u001b[1;32m 1905\u001b[0m \u001b[38;5;124;03m and ``parallel`` dictionary keys. The values must either be ``True`` or\u001b[39;00m\n\u001b[1;32m 1906\u001b[0m \u001b[38;5;124;03m ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is\u001b[39;00m\n\u001b[1;32m 1907\u001b[0m \u001b[38;5;124;03m ``{{'nopython': True, 'nogil': False, 'parallel': False}}``\u001b[39;00m\n\u001b[1;32m 1908\u001b[0m \n\u001b[1;32m 1909\u001b[0m \u001b[38;5;124;03m .. versionadded:: 1.4.0\u001b[39;00m\n\u001b[1;32m 1910\u001b[0m \n\u001b[1;32m 1911\u001b[0m \u001b[38;5;124;03m Returns\u001b[39;00m\n\u001b[1;32m 1912\u001b[0m \u001b[38;5;124;03m -------\u001b[39;00m\n\u001b[1;32m 1913\u001b[0m \u001b[38;5;124;03m pandas.Series or pandas.DataFrame\u001b[39;00m\n\u001b[1;32m 1914\u001b[0m \u001b[38;5;124;03m %(see_also)s\u001b[39;00m\n\u001b[1;32m 1915\u001b[0m \u001b[38;5;124;03m Examples\u001b[39;00m\n\u001b[1;32m 1916\u001b[0m \u001b[38;5;124;03m --------\u001b[39;00m\n\u001b[1;32m 1917\u001b[0m \u001b[38;5;124;03m >>> df = pd.DataFrame({'A': [1, 1, 2, 1, 2],\u001b[39;00m\n\u001b[1;32m 1918\u001b[0m \u001b[38;5;124;03m ... 'B': [np.nan, 2, 3, 4, 5],\u001b[39;00m\n\u001b[1;32m 1919\u001b[0m \u001b[38;5;124;03m ... 'C': [1, 2, 1, 1, 2]}, columns=['A', 'B', 'C'])\u001b[39;00m\n\u001b[1;32m 1920\u001b[0m \n\u001b[1;32m 1921\u001b[0m \u001b[38;5;124;03m Groupby one column and return the mean of the remaining columns in\u001b[39;00m\n\u001b[1;32m 1922\u001b[0m \u001b[38;5;124;03m each group.\u001b[39;00m\n\u001b[1;32m 1923\u001b[0m \n\u001b[1;32m 1924\u001b[0m \u001b[38;5;124;03m >>> df.groupby('A').mean()\u001b[39;00m\n\u001b[1;32m 1925\u001b[0m \u001b[38;5;124;03m B C\u001b[39;00m\n\u001b[1;32m 1926\u001b[0m \u001b[38;5;124;03m A\u001b[39;00m\n\u001b[1;32m 1927\u001b[0m \u001b[38;5;124;03m 1 3.0 1.333333\u001b[39;00m\n\u001b[1;32m 1928\u001b[0m \u001b[38;5;124;03m 2 4.0 1.500000\u001b[39;00m\n\u001b[1;32m 1929\u001b[0m \n\u001b[1;32m 1930\u001b[0m \u001b[38;5;124;03m Groupby two columns and return the mean of the remaining column.\u001b[39;00m\n\u001b[1;32m 1931\u001b[0m \n\u001b[1;32m 1932\u001b[0m \u001b[38;5;124;03m >>> df.groupby(['A', 'B']).mean()\u001b[39;00m\n\u001b[1;32m 1933\u001b[0m \u001b[38;5;124;03m C\u001b[39;00m\n\u001b[1;32m 1934\u001b[0m \u001b[38;5;124;03m A B\u001b[39;00m\n\u001b[1;32m 1935\u001b[0m \u001b[38;5;124;03m 1 2.0 2.0\u001b[39;00m\n\u001b[1;32m 1936\u001b[0m \u001b[38;5;124;03m 4.0 1.0\u001b[39;00m\n\u001b[1;32m 1937\u001b[0m \u001b[38;5;124;03m 2 3.0 1.0\u001b[39;00m\n\u001b[1;32m 1938\u001b[0m \u001b[38;5;124;03m 5.0 2.0\u001b[39;00m\n\u001b[1;32m 1939\u001b[0m \n\u001b[1;32m 1940\u001b[0m \u001b[38;5;124;03m Groupby one column and return the mean of only particular column in\u001b[39;00m\n\u001b[1;32m 1941\u001b[0m \u001b[38;5;124;03m the group.\u001b[39;00m\n\u001b[1;32m 1942\u001b[0m \n\u001b[1;32m 1943\u001b[0m \u001b[38;5;124;03m >>> df.groupby('A')['B'].mean()\u001b[39;00m\n\u001b[1;32m 1944\u001b[0m \u001b[38;5;124;03m A\u001b[39;00m\n\u001b[1;32m 1945\u001b[0m \u001b[38;5;124;03m 1 3.0\u001b[39;00m\n\u001b[1;32m 1946\u001b[0m \u001b[38;5;124;03m 2 4.0\u001b[39;00m\n\u001b[1;32m 1947\u001b[0m \u001b[38;5;124;03m Name: B, dtype: float64\u001b[39;00m\n\u001b[1;32m 1948\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m 1949\u001b[0m numeric_only_bool \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_resolve_numeric_only(numeric_only)\n", + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/ops.py:919\u001b[0m, in \u001b[0;36mapply_groupwise\u001b[0;34m(self, f, data, axis)\u001b[0m\n\u001b[1;32m 0\u001b[0m \n", + "Cell \u001b[0;32mIn[12], line 3\u001b[0m, in \u001b[0;36mchunk..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/dask/lib/python3.11/site-packages/pandas/core/series.py:1112\u001b[0m, in \u001b[0;36m__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 1108\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;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mindex, MultiIndex):\n\u001b[1;32m 1109\u001b[0m \u001b[38;5;66;03m# cases with MultiIndex don't get here bc they raise KeyError\u001b[39;00m\n\u001b[1;32m 1110\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(\n\u001b[1;32m 1111\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mkey of type tuple not found and not a MultiIndex\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m-> 1112\u001b[0m ) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01merr\u001b[39;00m\n\u001b[1;32m 1114\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m com\u001b[38;5;241m.\u001b[39mis_bool_indexer(key):\n", + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/series.py:1228\u001b[0m, in \u001b[0;36m_get_value\u001b[0;34m(self, label, takeable)\u001b[0m\n\u001b[1;32m 1227\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Return boolean indicating if self is cached or not.\"\"\"\u001b[39;00m\n\u001b[0;32m-> 1228\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mgetattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m_cacher\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m) \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/dask/lib/python3.11/site-packages/pandas/core/indexes/base.py:3812\u001b[0m, in \u001b[0;36mget_loc\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 0\u001b[0m \n", + "\u001b[0;31mKeyError\u001b[0m: 'EmployerSize'", + "\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[12], line 14\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m total \u001b[38;5;241m/\u001b[39m weights\n\u001b[1;32m 13\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---> 14\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[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[43m]\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43magg\u001b[49m\u001b[43m(\u001b[49m\u001b[43mextent\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mcompute()\n", + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/dask/utils.py:265\u001b[0m, in \u001b[0;36m_deprecated_kwarg.._deprecated_kwarg..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 263\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(msg)\n\u001b[1;32m 264\u001b[0m kwargs[new_arg_name] \u001b[38;5;241m=\u001b[39m new_arg_value\n\u001b[0;32m--> 265\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[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/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py:353\u001b[0m, in \u001b[0;36mnumeric_only_not_implemented..wrapper\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 343\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (\n\u001b[1;32m 344\u001b[0m PANDAS_GE_150\n\u001b[1;32m 345\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 346\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 347\u001b[0m ):\n\u001b[1;32m 348\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\n\u001b[1;32m 349\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 350\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 351\u001b[0m \u001b[38;5;167;01mFutureWarning\u001b[39;00m,\n\u001b[1;32m 352\u001b[0m )\n\u001b[0;32m--> 353\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/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py:3013\u001b[0m, in \u001b[0;36mDataFrameGroupBy.agg\u001b[0;34m(self, arg, split_every, split_out, shuffle_method, **kwargs)\u001b[0m\n\u001b[1;32m 3007\u001b[0m \u001b[38;5;129m@_deprecated_kwarg\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mshuffle\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mshuffle_method\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 3008\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 3009\u001b[0m \u001b[38;5;129m@numeric_only_not_implemented\u001b[39m\n\u001b[1;32m 3010\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21magg\u001b[39m(\n\u001b[1;32m 3011\u001b[0m \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_method\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[1;32m 3012\u001b[0m ):\n\u001b[0;32m-> 3013\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 3014\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 3015\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 3016\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 3017\u001b[0m \u001b[43m \u001b[49m\u001b[43mshuffle_method\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mshuffle_method\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3018\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 3019\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/dask/utils.py:265\u001b[0m, in \u001b[0;36m_deprecated_kwarg.._deprecated_kwarg..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 263\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(msg)\n\u001b[1;32m 264\u001b[0m kwargs[new_arg_name] \u001b[38;5;241m=\u001b[39m new_arg_value\n\u001b[0;32m--> 265\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[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/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py:2999\u001b[0m, in \u001b[0;36mDataFrameGroupBy.aggregate\u001b[0;34m(self, arg, split_every, split_out, shuffle_method, **kwargs)\u001b[0m\n\u001b[1;32m 2996\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 2997\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-> 2999\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 3000\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 3001\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 3002\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 3003\u001b[0m \u001b[43m \u001b[49m\u001b[43mshuffle_method\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mshuffle_method\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3004\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 3005\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/dask/utils.py:265\u001b[0m, in \u001b[0;36m_deprecated_kwarg.._deprecated_kwarg..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 263\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(msg)\n\u001b[1;32m 264\u001b[0m kwargs[new_arg_name] \u001b[38;5;241m=\u001b[39m new_arg_value\n\u001b[0;32m--> 265\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[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/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py:2448\u001b[0m, in \u001b[0;36m_GroupBy.aggregate\u001b[0;34m(self, arg, split_every, split_out, shuffle_method, **kwargs)\u001b[0m\n\u001b[1;32m 2441\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 2442\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mNotImplementedError\u001b[39;00m(\n\u001b[1;32m 2443\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 2444\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 2445\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 2446\u001b[0m )\n\u001b[0;32m-> 2448\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43maca\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2449\u001b[0m \u001b[43m \u001b[49m\u001b[43mchunk_args\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2450\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 2451\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 2452\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 2453\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 2454\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 2455\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 2456\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2457\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 2458\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 2459\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 2460\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 2461\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 2462\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 2463\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 2464\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2465\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 2466\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 2467\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 2468\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 2469\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 2470\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 2471\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 2472\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 2473\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 2474\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 2475\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 2476\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 2477\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 2478\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2480\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 2481\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/dask/lib/python3.11/site-packages/dask/dataframe/core.py:7127\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 7112\u001b[0m layer \u001b[38;5;241m=\u001b[39m DataFrameTreeReduction(\n\u001b[1;32m 7113\u001b[0m final_name,\n\u001b[1;32m 7114\u001b[0m chunked\u001b[38;5;241m.\u001b[39mname,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 7123\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 7124\u001b[0m )\n\u001b[1;32m 7126\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-> 7127\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 7128\u001b[0m meta \u001b[38;5;241m=\u001b[39m _emulate(\n\u001b[1;32m 7129\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 7130\u001b[0m )\n\u001b[1;32m 7131\u001b[0m meta \u001b[38;5;241m=\u001b[39m make_meta(\n\u001b[1;32m 7132\u001b[0m meta,\n\u001b[1;32m 7133\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 7134\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 7135\u001b[0m )\n", + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/dask/dataframe/core.py:7173\u001b[0m, in \u001b[0;36m_emulate\u001b[0;34m(func, udf, *args, **kwargs)\u001b[0m\n\u001b[1;32m 7168\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 7169\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 7170\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 7171\u001b[0m \u001b[38;5;124;03m dd.Series, using internal cache (``_meta``) for calculation\u001b[39;00m\n\u001b[1;32m 7172\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 7173\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 7174\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/dask/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/dask/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------------------------\nKeyError('EmployerSize')\n\nTraceback:\n---------\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/dask/dataframe/utils.py\", line 194, in raise_on_meta_error\n yield\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/dask/dataframe/core.py\", line 7174, in _emulate\n return func(*_extract_meta(args, True), **_extract_meta(kwargs, True))\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py\", line 1190, in _groupby_apply_funcs\n r = func(grouped, **func_kwargs)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py\", line 1266, in _apply_func_to_column\n return func(df_like[column])\n ^^^^^^^^^^^^^^^^^^^^^\n File \"/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_6760/1210873169.py\", line 4, in chunk\n return (chunk.apply(weighted_func), chunk.sum()[\"EmployerSize\"])\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/generic.py\", line 230, in apply\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/groupby.py\", line 1824, in apply\n Series or DataFrame\n ^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/groupby.py\", line 1885, in _python_apply_general\n \"\"\"\n \n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/groupby/ops.py\", line 919, in apply_groupwise\n File \"/var/folders/4n/v40br47s46ggrjm9bdm64lwh0000gn/T/ipykernel_6760/1210873169.py\", line 3, in weighted_func\n return (df[\"EmployerSize\"] * df[\"DiffMeanHourlyPercent\"]).sum()\n ~~^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/series.py\", line 1112, in __getitem__\n ) from err\n ^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/series.py\", line 1228, in _get_value\n return getattr(self, \"_cacher\", None) is not None\n ^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Users/luweizheng/miniconda3/envs/dask/lib/python3.11/site-packages/pandas/core/indexes/base.py\", line 3812, in get_loc\n" ] } ], @@ -1395,12 +1449,12 @@ " return total / weights\n", "\n", "extent = dd.Aggregation('extent', chunk, agg, finalize=finalize)\n", - "ddf.groupby(\"PostCode\")['EmployerSize', 'DiffMeanHourlyPercent'].agg(extent).compute()" + "ddf.groupby(\"PostCode\")[['EmployerSize', 'DiffMeanHourlyPercent']].agg(extent).compute()" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -1414,7 +1468,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -1423,22 +1477,10 @@ "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", + "foo 2.0 1.0 0.0 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", + "foo 2.0 1.0 0.0 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", @@ -1517,39 +1559,9 @@ "d 0 0.0" ] }, - "execution_count": 25, + "execution_count": 9, "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": [ @@ -1564,7 +1576,7 @@ " return maxima - minima\n", "\n", "extent = dd.Aggregation('extent', chunk, agg, finalize=finalize)\n", - "ddf.groupby('PostCode')[\"Size\", \"DiffMeanHourlyPercent\"].agg(extent).compute()" + "ddf.groupby('PostCode')[[\"Size\", \"DiffMeanHourlyPercent\"]].agg(extent).compute()" ] }, { @@ -1599,7 +1611,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -1609,12 +1621,12 @@ "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", + "Cell \u001b[0;32mIn[6], 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/dask/lib/python3.11/site-packages/dask/dataframe/core.py:5616\u001b[0m, in \u001b[0;36mDataFrame.groupby\u001b[0;34m(self, by, group_keys, sort, observed, dropna, **kwargs)\u001b[0m\n\u001b[1;32m 5604\u001b[0m \u001b[38;5;129m@derived_from\u001b[39m(pd\u001b[38;5;241m.\u001b[39mDataFrame)\n\u001b[1;32m 5605\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mgroupby\u001b[39m(\n\u001b[1;32m 5606\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 5612\u001b[0m \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs,\n\u001b[1;32m 5613\u001b[0m ):\n\u001b[1;32m 5614\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mdask\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mdataframe\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mgroupby\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m DataFrameGroupBy\n\u001b[0;32m-> 5616\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mDataFrameGroupBy\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 5617\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 5618\u001b[0m \u001b[43m \u001b[49m\u001b[43mby\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mby\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 5619\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 5620\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 5621\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 5622\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 5623\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 5624\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dask/lib/python3.11/site-packages/dask/dataframe/groupby.py:1477\u001b[0m, in \u001b[0;36m_GroupBy.__init__\u001b[0;34m(self, df, by, slice, group_keys, dropna, sort, observed)\u001b[0m\n\u001b[1;32m 1473\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mobserved[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mobserved\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m observed\n\u001b[1;32m 1475\u001b[0m \u001b[38;5;66;03m# raises a warning about observed=False with pandas>=2.1.\u001b[39;00m\n\u001b[1;32m 1476\u001b[0m \u001b[38;5;66;03m# We want to raise here, and not later down the stack.\u001b[39;00m\n\u001b[0;32m-> 1477\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 1478\u001b[0m \u001b[43m \u001b[49m\u001b[43mby_meta\u001b[49m\u001b[43m,\u001b[49m\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\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\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\n\u001b[1;32m 1479\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/miniconda3/envs/dask/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/dask/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/dask/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'" ] } @@ -1704,7 +1716,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.7" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/ch-ray-cluster/gpu.py b/ch-ray-cluster/gpu.py new file mode 100644 index 0000000..a1e4f5f --- /dev/null +++ b/ch-ray-cluster/gpu.py @@ -0,0 +1,20 @@ +import os +import ray + +ray.init() + +@ray.remote(num_gpus=1) +class GPUActor: + def ping(self): + print("GPU ids: {}".format(ray.get_runtime_context().get_accelerator_ids()["GPU"])) + print("CUDA_VISIBLE_DEVICES: {}".format(os.environ["CUDA_VISIBLE_DEVICES"])) + +@ray.remote(num_gpus=1) +def gpu_task(): + print("GPU ids: {}".format(ray.get_runtime_context().get_accelerator_ids()["GPU"])) + print("CUDA_VISIBLE_DEVICES: {}".format(os.environ["CUDA_VISIBLE_DEVICES"])) + +print("ENTRYPOINT CUDA_VISIBLE_DEVICES: {}".format(os.environ["CUDA_VISIBLE_DEVICES"])) +gpu_actor = GPUActor.remote() +ray.get(gpu_actor.ping.remote()) +ray.get(gpu_task.remote()) \ No newline at end of file diff --git a/ch-ray-cluster/index.md b/ch-ray-cluster/index.md new file mode 100644 index 0000000..0f88bc6 --- /dev/null +++ b/ch-ray-cluster/index.md @@ -0,0 +1,4 @@ +# Ray 集群 + +```{tableofcontents} +``` \ No newline at end of file diff --git a/ch-ray-cluster/pg.py b/ch-ray-cluster/pg.py new file mode 100644 index 0000000..f99108e --- /dev/null +++ b/ch-ray-cluster/pg.py @@ -0,0 +1,33 @@ +from ray.util.placement_group import ( + placement_group, + placement_group_table, + remove_placement_group, +) +from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy +import ray + +ray.init() + +print('''Available Resources: {}'''.format(ray.available_resources())) + +@ray.remote(num_gpus=2) +def gpu_task(): + print("GPU ids: {}".format(ray.get_runtime_context().get_accelerator_ids()["GPU"])) + +# 创建 Placement Group +pg = placement_group([{"CPU": 16, "GPU": 2}]) + +# 等待 Placement Group 创建成功 +ray.get(pg.ready(), timeout=10) +# 也可以使用 ray.wait +ready, unready = ray.wait([pg.ready()], timeout=10) + +print('''Placement Group: {}'''.format(placement_group_table(pg))) + +# 将 Ray Task 调度到这个 Placement Group +ray.get(gpu_task.options( + scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg) +).remote()) + +# 删除这个 Placement Group +remove_placement_group(pg) \ No newline at end of file diff --git a/ch-ray-cluster/ray-cluster.md b/ch-ray-cluster/ray-cluster.md new file mode 100644 index 0000000..9bad0b1 --- /dev/null +++ b/ch-ray-cluster/ray-cluster.md @@ -0,0 +1,77 @@ +(ray-cluster-resource)= +# Ray 集群 + +## Ray 集群 + +如 {numref}`ray-cluster` 所示,Ray 集群由一系列计算节点组成,其中两类关键的节点:头节点(Head node)和工作节点(Worker node)。这些节点可以部署在虚拟机、容器或者是裸金属服务器上。 + +```{figure} ../img/ch-ray-cluster/ray-cluster.svg +--- +width: 800px +name: ray-cluster +--- +Ray 集群由头节点和多个工作节点组成,头节点上运行着一些管理类的进程。 +``` + +所有节点上都运行着一些进程: + +* Worker + +每个计算节点上运行着一个或多个 Worker 进程,Worker 进程负责计算任务的运行。每个 Worker 进程运行特定的计算任务。Worker 进程或者是无状态的,即可以被反复执行 Remote Function 对应的 Task;又或者是一个 Actor,即只能执行有状态的 Remote Class 的方法。默认情况下,Worker 的数量等于其所在的计算节点的 CPU 核数。 + +* Raylet + +每个计算节点上运行着一个 Raylet。与一个计算节点上运行多个 Worker 进程不同,每个计算节点上只有一个 Raylet 进程,或者说 Raylet 被多个 Worker 进程所共享。Raylet 主要有两个组件:一个调度器(Scheduler),负责资源管理、任务分配等。各个计算节点上的 Scheduler 共同组成了整个 Ray 集群的分布式调度器;一个基于共享内存的对象存储(Share-memory Object Store),负责本地的数据存储,各个计算节点上的 Object Store 共同组成了 Ray 的分布式对象存储。 + +从 {numref}`ray-cluster` 中也可以看到,头节点还多了: + +* Global Control Service(GCS) + +GCS 是 Ray 集群的全局元数据管理服务,这里的元数据信息比如某个 Actor 被分配到哪个计算节点上。它管理的元数据是所有 Worker 共享的。 + +* Driver + +Driver 执行的是程序的入口,比如,作为 Python 入口的 `__main__` 函数。一般情况下,`__main__` 函数运行时不执行大规模的计算,只是把 Task 和 Actor 调度到具有足够资源的 Worker 上。 + +Ray 的头节点还运行着其他一些管理类的服务,比如计算资源自动缩放、作业提交等服务。 + +## 启动 Ray 集群 + +之前在 Python 代码中使用 `ray.init()` 方式,仅在本地启动了一个单机的 Ray 集群。实际上,Ray 集群包括头节点和工作节点,应该分别启动。先在头节点启动: + +```bash +ray start --head --port=6379 +``` + +它会在该物理节点启动一个头节点进程,默认端口号是 6379,也可以用 `--port` 来指定端口号。执行完上述命令后,命令行会有一些提示,包括当前节点的地址,如何关停,如何启动其他工作节点: + +```bash +ray start --address=: +``` + +将 `:` 替换为刚刚启动的 Ray 头节点的地址。 + +此外,Ray 还提供了 `ray up` 这种集群启动命令,它接收 yaml 文件作为参数,在 yaml 文件里定义好头节点地址、工作节点地址。一个文件的样例 [example.yaml](https://raw.githubusercontent.com/ray-project/ray/master/python/ray/autoscaler/local/example-full.yaml): + +```yaml +cluster_name: default + +provider: + type: local + head_ip: YOUR_HEAD_NODE_HOSTNAME + worker_ips: [WORKER_NODE_1_HOSTNAME, WORKER_NODE_2_HOSTNAME, ... ] +``` + +使用下面的命令,它会帮我们启动这个 Ray 集群: + +``` +ray up example.yaml +``` + +可以用 `ray status` 查看启动的 Ray 集群的状态。 + +:::{note} +Ray 的头节点暴露了三个端口号,默认分别是 6379, 8265, 10001。启动 Ray 时,设置了 Ray 头节点的端口号,默认为 6379,这个端口号是头节点和工作节点之间通信的端口。Ray 头节点启动后,还提供了一个 Ray 仪表盘端口号,默认为 8265,这个端口号可用来接收 Ray 命令行提交的作业。此外,还有一个端口 `10001`,默认为 `ray.init()` 连接时使用。 +::: + +以上方法可在多台虚拟机或物理机上部署一个 Ray 集群,Ray 也提供了 Kubernetes 和配套工具,可以支持自动缩放。 \ No newline at end of file diff --git a/ch-ray-cluster/ray-job.md b/ch-ray-cluster/ray-job.md new file mode 100644 index 0000000..1eaca82 --- /dev/null +++ b/ch-ray-cluster/ray-job.md @@ -0,0 +1,171 @@ +(ray-job)= +# Ray 作业 + +部署好一个 Ray 集群后,我们就可以向集群上提交作业了。Ray 作业指的是用户编写的,基于 Task、Actor 或者 Ray 各类生态(Ray Train 等)的具体的计算任务。向 Ray 集群上提交作业主要有三类方式: + +* Ray Jobs 命令行 +* Python Software Development Kit (SDK) +* Ray 客户端 + +一个 Ray 作业除了需要 `__main__` 函数的入口外,还需要: + +* 工作目录:这个作业所需要的 Python 代码和配置文件 +* 软件环境:这个作业所依赖的 Python 软件包和环境变量 + +## Ray Jobs 命令行 + +### `ray job` + +Ray Jobs 命令行指的是 `ray job` 一系列操作作业的脚本。在 Python 环境中安装好 Ray 之后(`pip install "ray[default]"`),也会安装命令行工具,其中 `ray job` 负责作业的全生命周期管理。 + +我们先写好一个基于 Ray 的脚本,放置在当前目录 `./` 下,名为 `scripy.py`: + +```python +import os + +import ray + +ray.init() + +print('''This cluster consists of + {} nodes in total + {} CPU resources in total +'''.format(len(ray.nodes()), ray.cluster_resources()['CPU'])) + +@ray.remote +def generate_fibonacci(sequence_size): + fibonacci = [] + for i in range(0, sequence_size): + if i < 2: + fibonacci.append(i) + continue + fibonacci.append(fibonacci[i-1] + fibonacci[i-2]) + return len(fibonacci) + +sequence_size = 10 +results = ray.get([generate_fibonacci.remote(sequence_size) for _ in range(os.cpu_count())]) +print(results) +``` +使用 `ray job submit` 提交这个作业: + +```bash +RAY_ADDRESS='http://127.0.0.1:8265' ray job submit --working-dir ./ -- python script.py +``` + +`RAY_ADDRESS` 根据头节点的地址来设定,如果只有本地的 Ray 集群,头节点的 IP 地址是 `127.0.0.1`,默认端口是 8265,那么这个地址为 `http://127.0.0.1:8265` ;假如有一个远程的集群,地址修改为远程集群的 IP 或主机名。 + +Ray Job 命令行将工作目录 `./` 下的源代码打包,将该作业提交到集群上,并打印出下面的信息: + +``` +Job submission server address: http://127.0.0.1:8265 +INFO dashboard_sdk.py:338 -- Uploading package gcs://_ray_pkg_bd62811ee3a826e8.zip. +INFO packaging.py:530 -- Creating a file package for local directory './'. + +------------------------------------------------------- +Job 'raysubmit_VTRVfy8VEFY8vCdn' submitted successfully +------------------------------------------------------- +``` + +`ray job submit` 的格式为:`ray job submit [OPTIONS] ENTRYPOINT...`。 `[OPTIONS]` 可以指定一些参数。 `--working-dir` 为工作目录,Ray 会将该目录下的内容打包,分发到 Ray 集群各个节点。`ENTRYPOINT` 指的是需要执行的 Python 脚本,本例中,是 `python script.py`。我们还可以给这个 Python 脚本传参数,就跟在单机执行 Python 脚本一样:`python script.py --arg=val`。 + +`--no-wait` 参数可以先提交作业到 Ray 集群,而不是一直等待作业结束。作业的结果可以通过 `ray job logs ` 查看。 + +:::{note} +`ENTRYPOINT` 和 `[OPTIONS]` 之间有空格。 +::: + +### 入口 + +`ENTRYPOINT` 是程序的入口,在刚才的例子中,程序的入口就是调用 `generate_fibonacci` 的 Ray Task,Ray Task 会被调度到 Ray 集群上。默认情况下,`ENTRYPOINT` 中的入口部分在头节点上运行,因为头节点的资源有限,不能执行各类复杂的计算,只能起到一个入口的作用,各类复杂计算应该在 Task 或 Actor 中执行。默认情况下,无需额外的配置,Ray 会根据 Task 或 Actor 所设置的资源需求,将这些计算调度到计算节点上。但如果 `ENTRYPOINT` 的入口(调用 Task 或 Actor 之前)就使用了各类资源,比如 GPU,那需要给这个入口脚本额外分配资源,需要在 `[OPTIONS]` 中设置 `--entrypoint-num-cpus`、`--entrypoint-num-gpus` 或者 `--entrypoint-resources`。比如,下面的例子分配了 1 个 GPU 给入口。 + +``` +RAY_ADDRESS='http://127.0.0.1:8265' ray job submit --working-dir ./ --entrypoint-num-gpus 1 -- python gpu.py +``` + +其中 `gpu.py` 代码如下: + +```python +import os +import ray + +ray.init() + +@ray.remote(num_gpus=1) +class GPUActor: + def ping(self): + print("GPU ids: {}".format(ray.get_runtime_context().get_accelerator_ids()["GPU"])) + print("CUDA_VISIBLE_DEVICES: {}".format(os.environ["CUDA_VISIBLE_DEVICES"])) + +@ray.remote(num_gpus=1) +def gpu_task(): + print("GPU ids: {}".format(ray.get_runtime_context().get_accelerator_ids()["GPU"])) + print("CUDA_VISIBLE_DEVICES: {}".format(os.environ["CUDA_VISIBLE_DEVICES"])) + +print("ENTRYPOINT CUDA_VISIBLE_DEVICES: {}".format(os.environ["CUDA_VISIBLE_DEVICES"])) +gpu_actor = GPUActor.remote() +ray.get(gpu_actor.ping.remote()) +ray.get(gpu_task.remote()) +``` + +调用 Actor 和 Task 之前,Ray 分配了一个 GPU 给程序的入口。调用 Actor 和 Task 之后,又分别给 `gpu_actor` 和 `gpu_task` 分配了 1 个 GPU。 + +:::{note} +将提交作业到一个已有的 Ray 集群上,`ray.init()` 中不能设置 `num_cpus` 和 `num_gpus` 参数。 +::: + +### 依赖管理 + +Ray 集群中可能运行着不同的作业,不同作业对 Python 各个依赖的版本要求不同,Ray 提供了运行时环境的功能,比如在启动这个作业时,设置 `--runtime-env-json`,他是一个 JSON,包括:需要 `pip` 安装的 Python 包,或环境变量(`env_vars`),或工作目录(`working_dir`)。 + +```json +{ + "pip": ["requests==2.26.0"], + "env_vars": {"TF_WARNINGS": "none"} +} +``` + +## Python SDK + +Python SDK 的底层原理与命令行相似,只不过将提交作业的各类参数写在 Python 代码中,执行 Python 代码来提交作业。SDK 提供了一个客户端,用户在客户端调用 `ray.job_submission.JobSubmissionClient` 来传递作业参数。 + +```python +import time +from ray.job_submission import JobSubmissionClient, JobStatus + +client = JobSubmissionClient("http://127.0.0.1:8265") +job_id = client.submit_job( + entrypoint="python script.py", + runtime_env={"working_dir": "./"} +) +print(job_id) + +def wait_until_status(job_id, status_to_wait_for, timeout_seconds=5): + start = time.time() + while time.time() - start <= timeout_seconds: + status = client.get_job_status(job_id) + print(f"status: {status}") + if status in status_to_wait_for: + break + time.sleep(1) + + +wait_until_status(job_id, {JobStatus.SUCCEEDED, JobStatus.STOPPED, JobStatus.FAILED}) +logs = client.get_job_logs(job_id) +print(logs) +``` + +[`JobSubmissionClient.submit_job()`](https://docs.ray.io/en/latest/cluster/running-applications/job-submission/doc/ray.job_submission.JobSubmissionClient.submit_job.html) 作业提交是异步的,Ray 会马上返回作业的 ID。如果想要看到作业的运行情况,需要 `wait_until_status()` 函数,不断向 Ray 集群请求,查看该作业的状态。跟命令行类似,`submit_job()` 中传入 `runtime_env` 来指定工作目录或依赖的 Python 包;`entrypoint_num_cpus` 和 `entrypoint_num_gpus` 指定入口所需要的计算资源。 + +## Ray 客户端 + +Ray 客户端指的是在 Python 的 `ray.init()` 中直接指定 Ray 集群的地址:`ray.init("ray://:")`。 + +:::{note} +注意,这里的 `port` 默认为 10001,或者在 `ray start --head` 时对 `--ray-client-server-port` 进行设置。 +::: + +客户端可以运行在个人电脑上,用户可以交互地调用集群的计算资源。需要注意的是,客户端的一些功能不如命令行和 SDK 完善,复杂的任务应优先使用命令行或者 SDK。 + +`ray.init()` 也接收 `runtime_env` 参数,用来指定 Python 包版本或工作目录。跟 Ray Jobs 命令行一样,Ray 会将工作目录中的数据传到 Ray 集群上。 + +如果客户端与 Ray 集群的连接断开,这个客户端创建的分布式对象或引用都会被销毁。如果客户端和 Ray 集群意外断开,Ray 会在 30 秒后重新连接,重新连接失败后会把各类引用销毁。环境变量 `RAY_CLIENT_RECONNECT_GRACE_PERIOD` 可对这个时间进行自定义。 \ No newline at end of file diff --git a/ch-ray-cluster/ray-resource.md b/ch-ray-cluster/ray-resource.md new file mode 100644 index 0000000..6f862e5 --- /dev/null +++ b/ch-ray-cluster/ray-resource.md @@ -0,0 +1,137 @@ +(ray-computing-resource)= +# 计算资源与资源组 + +## 计算资源 + +Ray 可以管理计算资源,包括 CPU、内存和 GPU 等各类加速器。这里的计算资源是逻辑上的,逻辑资源与物理上的计算资源相对应。Ray 集群的各个节点启动时会探测物理计算资源,并根据一定规则映射为逻辑上的计算资源: + +* CPU:每个节点(容器)中的物理 CPU 个数(`num_cpus`) +* GPU:每个节点(容器)中的物理 GPU 个数(`num_gpus`) +* 内存:每个节点可用内存的 70%(`memory`) + +以上为默认的规则。也可以在启动 Ray 集群时,手动指定这些资源。比如某台物理节点上有 64 个 CPU 核心,8 个 GPU,启动 Ray 工作节点时只注册一部分计算资源。 + +``` +ray start --num-cpus=32 --num-gpus=4 +``` + +## 资源需求 + +默认情况下,Ray Task 会使用 1 个逻辑 CPU,这 1 个 CPU 既用来调度,又用来运行计算任务;Ray Actor 会使用 1 个逻辑 CPU 用来调度,0 个 CPU 用来运行计算任务。Task 或 Actor 在执行时,会被 Ray 调度到满足需求的节点上。在默认情况下,Ray Task 的资源需求比较明确,而 Ray Actor 只需要 0 个 CPU 用来运行计算任务,可能导致无限个 Ray Actor 运行到一台工作节点上。对于某个具体的计算任务,可以在定义 Task 或者 Actor 时,指定使用多少计算资源;指定计算资源的数量有利于计算任务的调度和运行,避免出现一些不确定的风险。具体而言,使用 `ray.remote()` 修饰函数或类时,传入 `num_cpus` 和 `num_gpus` 参数,可以指定 Task 和 Actor 所需资源。 + +``` +@ray.remote(num_cpus=4) +def func(): + ... + +@ray.remote(num_cpus=16, num_gpus=1) +class Actor: + pass +``` + +或者调用 `task.options()` 或 `actor.options()` 来指定某个具体计算任务所需的资源,其中 `task` 是经过 `ray.remote()` 修饰的分布式函数,`actor` 是经过 `ray.remote()` 修饰的分布式类的实例。 + +``` +func.options(num_cpus=4).remote() +``` + +## 其他资源 + +除了通用的 CPU、GPU 外,Ray 也支持很多其他各类计算资源,比如各类加速器。可以使用 `--resources={"special_hardware": 1}` 这样的键值对来管理这些计算资源。使用方式与 `num_gpus` 管理 GPU 资源相似。比如 Google 的 TPU (比如:`resources={"TPU": 2}`)和华为的昇腾(`resources={"NPU": 2}`)。或者某集群得 CPU 既有 x86 架构,也有 ARM 架构,也可以这样定义:`resources={"arm64": 1}`。 + +## 自动缩放 + +Ray 集群可以自动缩放,主要面向以下场景: + +* 当 Ray 集群的资源不够时,创建新的工作节点。 +* 当某个工作节点闲置或者无法启动,将该工作节点关闭。 + +自动缩放主要满足 Task 或 Actor 中对计算资源需求,而不是根据计算节点的资源利用情况自动缩放。 + +## Placement Group + +基于计算资源和集群,Ray 提供了 Placement Group,中文可以理解成资源组。Placement Groups 允许用户**原子地**使用集群上多个节点的计算资源,所谓原子地(Atomically),是指这些资源或者都分配给该用户,或者完全不分配,不会出现只分配一部分的情况。 + +Placement Groups 主要针对的场景案例有: + +* 一个作业需要一组资源,这些资源需要协同工作以完成任务,给这个作业分配一部分资源,无法完成任务。这种场景在集群调度中又被称为组调度(Gang Scheduling)。比如,大规模分布式训练中需要多台计算节点和多块 GPU,需要在 Ray 集群中申请并分配这些资源。 + +* 作业需要在多个节点上负载均衡,每个节点承担一小部分任务。Placement Groups 使得这个作业尽量分摊到多个计算节点上。比如,在一个分布式推理场景,一个作业需要 8 块 GPU,每个 GPU 加载模型,独立地进行推理。为了负载均衡,应该将作业调度到 8 个计算节点上,每个节点占用 1 块 GPU;而不是将这个作业调度到 1 个计算节点的 8 块 GPU 上。因为都调度到 1 个计算节点,节点故障后,整个推理服务不可用。 + +Placement Groups 有几个关键概念: + +* 资源包(Bundle):Bundle 一个键值对,用来定义所需的计算资源,比如 `{"CPU": 2}`,或 `{"CPU": 8, "GPU": 4}`。一个 Bundle 必须可以调度到单个计算节点;比如,一个计算节点只有 8 块 GPU,`{"GPU": 10}` 这样的 Bundle 是不合理的。 +* 资源组(Placement Group):Placement Group 是一组 Bundle。比如,`{"CPU": 8} * 4` 会向 Ray 集群申请 4 个 Bundle,每个 Bundle 预留 8 个 CPU。多个 Bundle 的调度会遵循一些调度策略。Placement Group 被 Ray 集群创建后,可被用来运行 Ray Task 和 Ray Actor。 + +我们可以使用 [`placement_group()`](https://docs.ray.io/en/latest/ray-core/api/doc/ray.util.placement_group.html) 创建 Placement Group。`placement_group()` 是异步的,如果需要等待创建成功,需要调用 [`PlacementGroup.ready()`](https://docs.ray.io/en/latest/ray-core/api/doc/ray.util.placement_group.PlacementGroup.ready.html)。 + +某个 Ray Task 或 Ray Actor 希望调度到 Placement Group 上,可以在 `options(scheduling_strategy=PlacementGroupSchedulingStrategy(...))` 中设定。 + +下面是一个完整的例子,运行这个例子之前,提前创建好了有多块 GPU 的 Ray 集群,如果没有 GPU,也可以改为 CPU。 + +```python +from ray.util.placement_group import ( + placement_group, + placement_group_table, + remove_placement_group, +) +from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy +import ray + +ray.init() + +print('''Available Resources: {}'''.format(ray.available_resources())) + +@ray.remote(num_gpus=2) +def gpu_task(): + print("GPU ids: {}".format(ray.get_runtime_context().get_accelerator_ids()["GPU"])) + +# 创建 Placement Group +pg = placement_group([{"CPU": 16, "GPU": 2}]) + +# 等待 Placement Group 创建成功 +ray.get(pg.ready(), timeout=10) +# 也可以使用 ray.wait +ready, unready = ray.wait([pg.ready()], timeout=10) + +print('''Placement Group: {}'''.format(placement_group_table(pg))) + +# 将 Ray Task 调度到这个 Placement Group +ray.get(gpu_task.options( + scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg) +).remote()) + +# 删除这个 Placement Group +remove_placement_group(pg) +``` + +创建 Placement Group 的 `placement_group()` 方法还接收 `strategy` 参数,用来设定不同的调度策略:或者是让这些预留资源尽量集中到少数计算节点上,或者是让这些预留资源尽量分散到多个计算节点。共有如下策略: + +* `STRICT_PACK`:所有 Bundle 都必须调度到单个计算节点。 +* `PACK`:所有 Bundle 优先调度到单个计算节点,如果无法满足条件,再调度到其他计算节点,如 {numref}`ray-pg-pack` 所示。如果不做设置,是默认的调度策略。 +* `STRICT_SPREAD`:每个 Bundle 必须调度到不同的计算节点。 +* `SPREAD`:每个 Bundle 优先调度到不同的计算节点,如果无法满足条件,有些 Bundle 可以共用一个计算节点,如 {numref}`ray-pg-spread` 所示。 + +```{figure} ../img/ch-ray-cluster/pg-pack.png +--- +width: 600px +name: ray-pg-pack +--- +`PACK` 策略优先将所有 Bundle 调度到单个计算节点。 +``` + +由于计算尽量调度到了少数计算节点,`STRICT_PACK` 和 `PACK` 的调度策略保证了数据的局部性(Data Locality),计算任务可以快速访问本地的数据。 + +```{figure} ../img/ch-ray-cluster/pg-spread.png +--- +width: 600px +name: ray-pg-spread +--- +`SPREAD` 策略优先将每个 Bundle 调度到不同的计算节点。 +``` + +`STRICT_SPREAD` 和 `SPREAD` 的调度策略使得计算更好地负载均衡。 + +:::{note} +多个 Ray Task 或 Actor 可以运行在同一个 Bundle 上,任何使用同一个 Bundle 的 Task 或 Actor 将一直运行在该计算节点上。 +::: diff --git a/ch-ray-cluster/script.py b/ch-ray-cluster/script.py new file mode 100644 index 0000000..386496a --- /dev/null +++ b/ch-ray-cluster/script.py @@ -0,0 +1,23 @@ +import os +import ray + +ray.init() + +print('''This cluster consists of + {} nodes in total + {} CPU resources in total +'''.format(len(ray.nodes()), ray.cluster_resources()['CPU'])) + +@ray.remote +def generate_fibonacci(sequence_size): + fibonacci = [] + for i in range(0, sequence_size): + if i < 2: + fibonacci.append(i) + continue + fibonacci.append(fibonacci[i-1] + fibonacci[i-2]) + return len(fibonacci) + +sequence_size = 10 +results = ray.get([generate_fibonacci.remote(sequence_size) for _ in range(os.cpu_count())]) +print(results) \ No newline at end of file diff --git a/ch-ray-cluster/sdk.py b/ch-ray-cluster/sdk.py new file mode 100644 index 0000000..1018dcc --- /dev/null +++ b/ch-ray-cluster/sdk.py @@ -0,0 +1,23 @@ +import time +from ray.job_submission import JobSubmissionClient, JobStatus + +client = JobSubmissionClient("http://127.0.0.1:8265") +job_id = client.submit_job( + entrypoint="python script.py", + runtime_env={"working_dir": "./"} +) +print(job_id) + +def wait_until_status(job_id, status_to_wait_for, timeout_seconds=5): + start = time.time() + while time.time() - start <= timeout_seconds: + status = client.get_job_status(job_id) + print(f"status: {status}") + if status in status_to_wait_for: + break + time.sleep(1) + + +wait_until_status(job_id, {JobStatus.SUCCEEDED, JobStatus.STOPPED, JobStatus.FAILED}) +logs = client.get_job_logs(job_id) +print(logs) \ No newline at end of file diff --git a/ch-ray-core/ray-internal.md b/ch-ray-core/ray-internal.md deleted file mode 100644 index 40786e7..0000000 --- a/ch-ray-core/ray-internal.md +++ /dev/null @@ -1,34 +0,0 @@ -(ray-internal)= -# Ray 系统与设计 - -## Ray 集群 - -如 {numref}`ray-cluster` 所示,Ray 集群由一系列计算节点组成,其中两类关键的节点:头节点(Head node)和工作节点(Worker node)。这些节点可以部署在虚拟机、容器或者是裸金属服务器上。 - -```{figure} ../img/ch-ray-core/ray-cluster.svg ---- -width: 800px -name: ray-cluster ---- -Ray 集群 -``` - -所有节点上都运行着一些进程: - -* Worker - -每个计算节点上运行着一个或多个 Worker 进程,Worker 进程负责计算任务的运行。每个 Worker 进程运行特定的计算任务。Worker 进程或者是无状态的,即可以被反复执行 Remote Function 对应的 Task;又或者是一个 Actor,即只能执行有状态的 Remote Class 的方法。默认情况下,Worker 的数量等于其所在的计算节点的 CPU 核数。 - -* Raylet - -每个计算节点上运行着一个 Raylet。与一个计算节点上运行多个 Worker 进程不同,每个计算节点上只有一个 Raylet 进程,或者说 Raylet 被多个 Worker 进程所共享。Raylet 主要有两个组件:一个调度器(Scheduler),负责资源管理、任务分配等。各个计算节点上的 Scheduler 共同组成了整个 Ray 集群的分布式调度器;一个基于共享内存的对象存储(Share-memory Object Store),负责本地的数据存储,各个计算节点上的 Object Store 共同组成了 Ray 的分布式对象存储。 - -从 {numref}`ray-cluster` 中也可以看到,头节点还多了: - -* Global Control Service(GCS) - -GCS 是 Ray 集群的全局的元数据管理服务,这里的元数据信息比如某个 Actor 被分配到哪个计算节点上。它管理的元数据是所有 Worker 共享的。 - -* Driver - -Driver 执行一些顶层的应用,比如,作为 Python 入口的 `__main__` 函数。 \ No newline at end of file diff --git a/conf.py b/conf.py index 0b4d7e9..47937e7 100644 --- a/conf.py +++ b/conf.py @@ -8,7 +8,7 @@ external_toc_exclude_missing = True external_toc_path = '_toc.yml' html_baseurl = '' -html_favicon = '' +html_favicon = "_static/logo.ico" html_logo = 'logo.svg' html_sourcelink_suffix = '' html_theme = 'sphinx_book_theme' diff --git a/drawio/ch-ray-core/ray-cluster.drawio b/drawio/ch-ray-cluster/ray-cluster.drawio similarity index 84% rename from drawio/ch-ray-core/ray-cluster.drawio rename to drawio/ch-ray-cluster/ray-cluster.drawio index 59dcd0d..b796ce7 100644 --- a/drawio/ch-ray-core/ray-cluster.drawio +++ b/drawio/ch-ray-cluster/ray-cluster.drawio @@ -1,16 +1,16 @@ - + - + - + - + - + @@ -23,7 +23,7 @@ - + @@ -31,13 +31,13 @@ - + - + @@ -55,13 +55,13 @@ - + - + @@ -79,9 +79,12 @@ - + + + + diff --git a/img/ch-ray-cluster/pg-pack.png b/img/ch-ray-cluster/pg-pack.png new file mode 100644 index 0000000000000000000000000000000000000000..e15376e96ede0f344e36c6c3fb7321afcc00198a GIT binary patch literal 40047 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ÐPu0Z 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 literal 0 HcmV?d00001 diff --git a/img/ch-ray-cluster/pg-spread.png b/img/ch-ray-cluster/pg-spread.png new file mode 100644 index 0000000000000000000000000000000000000000..bb733053ebf43432ab6de758080c02dc6fbad71f GIT binary patch literal 38558 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 literal 0 HcmV?d00001 diff --git a/img/ch-ray-cluster/ray-cluster.svg b/img/ch-ray-cluster/ray-cluster.svg new file mode 100644 index 0000000..f811666 --- /dev/null +++ b/img/ch-ray-cluster/ray-cluster.svg @@ -0,0 +1,4 @@ + + + +
Driver
Driver
Worker
Worker
Scheduler
Scheduler
Object Store
Object Store
Global Control Service
Global Control Service
Raylet
Raylet
头节点
头节点
Worker
Worker
Worker
Worker
Scheduler
Scheduler
Object Store
Object Store
Raylet
Raylet
工作节点
工作节点
Worker
Worker
Worker
Worker
Scheduler
Scheduler
Object Store
Object Store
Raylet
Raylet
工作节点
工作节点
Autoscaler
Autoscaler
Text is not SVG - cannot display
\ No newline at end of file diff --git a/img/ch-ray-core/ray-cluster.svg b/img/ch-ray-core/ray-cluster.svg deleted file mode 100644 index b906e52..0000000 --- a/img/ch-ray-core/ray-cluster.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
Driver
Driver
Worker
Worker
Scheduler
Scheduler
Object Store
Object Store
Global Control Service
Global Control Service
Raylet
Raylet
头结点
头结点
Driver
Driver
Worker
Worker
Scheduler
Scheduler
Object Store
Object Store
Raylet
Raylet
工作结点
工作结点
Driver
Driver
Worker
Worker
Scheduler
Scheduler
Object Store
Object Store
Raylet
Raylet
工作结点
工作结点
Text is not SVG - cannot display
\ No newline at end of file