diff --git a/autogluon-config.toml b/autogluon-config.toml
new file mode 100644
index 0000000..ac13eb9
--- /dev/null
+++ b/autogluon-config.toml
@@ -0,0 +1,14 @@
+[tool.entropice-autogluon]
+time-limit = 60
+presets = "medium"
+target = "darts_v1"
+task = "density"
+experiment = "tobi-tests"
+
+grid = "hex"
+level = 5
+members = ["ERA5-shoulder", "ArcticDEM"]
+
+[tool.entropice-autogluon.dimension-filters]
+# ERA5-shoulder = { aggregations = "median" }
+ArcticDEM = { aggregations = "median" }
diff --git a/hpsearchcv-config.toml b/hpsearchcv-config.toml
new file mode 100644
index 0000000..f0d0dc8
--- /dev/null
+++ b/hpsearchcv-config.toml
@@ -0,0 +1,19 @@
+[tool.entropice-hpsearchcv]
+n-iter = 5
+target = "darts_v1"
+task = "binary"
+splitter = "kfold"
+# model = "xgboost"
+# model = "rf" SHAP error
+model = "espa"
+experiment = "tobi-tests"
+scaler = "standard" # They dont work because of Array API
+normalize = true
+
+grid = "hex"
+level = 5
+members = ["ERA5-shoulder", "ArcticDEM"]
+
+[tool.entropice-hpsearchcv.dimension-filters]
+# ERA5-shoulder = { aggregations = "median" }
+ArcticDEM = { aggregations = "median" }
diff --git a/pixi.lock b/pixi.lock
index 4b30b5c..a9d82e3 100644
--- a/pixi.lock
+++ b/pixi.lock
@@ -257,21 +257,22 @@ environments:
- pypi: https://files.pythonhosted.org/packages/31/4a/72dc383d1a0d14f1d453e334e3461e229762edb1bf3f75b3ab977e9386ed/arro3_core-0.6.5-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/1b/df/2a5a1306dc1699b51b02c1c38c55f3564a8c4f84087c23c61e7e7ae37dfa/arro3_io-0.6.5-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/c3/1c/f06ad85180e7dd9855aa5ede901bfc2be858d7bee17d4e978a14c0ecec14/astropy-7.2.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/57/61/2d06c08f022c9b617b79f6c55d88e596c1795a1d211e6bf584ac4b9e9506/astropy_iers_data-0.2026.1.5.0.43.43-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/74/51/59effa402d4ce8813e42eb62416059d42dd07826b0e7aa2db057c336972d/astropy_iers_data-0.2026.2.2.0.48.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/2d/1b/37d8a28965907d23eeba8bce56272932ee01176d192cefdf19a4a0b53c00/autogluon_common-1.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/96/de/4bffa0f6f3257e73a22402019d19fbe34dfedc2865896f97ad57935cf7dd/autogluon_core-1.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f3/c8/46eb69e371da89337419d3c754140f3ddae3c85a81b061ba3f275f442475/autogluon_features-1.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/48/7c/50547d2940e98c8a15b8c92cd4953814385b95f5fc1dec806fa240389417/autogluon_tabular-1.5.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/ee/34/a9914e676971a13d6cc671b1ed172f9804b50a3a80a143ff196e52f4c7ee/azure_core-1.37.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/d8/3a/6ef2047a072e54e1142718d433d50e9514c999a58f51abfff7902f3a72f8/azure_storage_blob-12.28.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/bf/51/472e7b36a6bedb5242a9757e7486f702c3619eff76e256735d0c8b1679c6/blis-1.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/f6/a8/877f306720bc114c612579c5af36bcb359026b83d051226945499b306b1a/bokeh-3.8.2-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/96/9a/663251dfb35aaddcbdbef78802ea5a9d3fad9d5fadde8774eacd9e1bfbb7/boost_histogram-1.6.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/9b/fa/a8198c3f4cb4a4989394383590dcdbaaad0be1660e8800016cbb7bd91f08/boost_histogram-1.7.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/3c/56/f47a80254ed4991cce9a2f6d8ae8aafbc8df1c3270e966b2927289e5a12f/boto3-1.41.5-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4e/4e/21cd0b8f365449f1576f93de1ec8718ed18a7a3bc086dfbdeb79437bba7a/botocore-1.41.5-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/bd/22/05555a9752357e24caa1cd92324d1a7fdde6386aab162fcc451f8f8eedc2/bottleneck-1.6.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
@@ -281,9 +282,9 @@ environments:
- pypi: https://files.pythonhosted.org/packages/9e/96/d32b941a501ab566a16358d68b6eb4e4acc373fab3c3c4d7d9e649f7b4bb/catalogue-2.0.10-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/0c/4b/d04e067df4401902cb2249df241bc1502bf90e990c6a3da5f82ba7de60fa/catboost-1.2.8-cp313-cp313-manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/4f/f4/4a65460d5cb6784128019fd707a87993f378db25e796eba01400a0903f62/cdsapi-0.7.7-py2.py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/27/27/6414b1b7e5e151300c54e28ad1cf3e3b34fe66dc3256a989b031166b1ba3/cdshealpix-0.7.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/ce/0c/00c768197bbcba5835f22b69c2611cb422c0f8a9dc93af62e26d14584b29/cdshealpix-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/a3/8f/c42a98f933022c7de00142526c9b6b7429fdcd0fc66c952b4ebbf0ff3b7f/cf_xarray-0.10.10-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/65/ce/5c4f4660da5521d90bea62cdf8396d7e4ce4a00513e218d267b97f9ea453/cf_xarray-0.10.11-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/ba/08/52f06ff2f04d376f9cd2c211aefcf2b37f1978e43289341f362fc99f6a0e/cftime-1.6.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
@@ -297,43 +298,46 @@ environments:
- pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/0c/00/3106b1854b45bd0474ced037dfe6b73b90fe68a68968cef47c23de3d43d2/confection-0.1.5-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fa/25/0be9314cd72fe2ee2ef89ceb1f438bc156428a12177d684040456eee4a56/cupy_xarray-0.1.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/20/5b/0eceb9a5990de9025733a0d212ca43649ba9facd58b8552b6bf93c11439d/cyclopts-4.4.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/1c/7c/996760c30f1302704af57c66ff2d723f7d656d0d0b93563b5528a51484bb/cyclopts-4.5.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/36/36/bc980b9a14409f3356309c45a8d88d58797d02002a9d794dd6c84e809d3a/cymem-2.0.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/6f/3a/2121294941227c548d4b5f897a8a1b5f4c44a58f5437f239e6b86511d78e/dask-2025.12.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/e5/23/d39ccc4ed76222db31530b0a7d38876fdb7673e23f838e8d8f0ed4651a4f/dask-2026.1.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/28/0e/b11ad5fd77e3dd0baad9cac3184315be7654ae401e3b0b0c324503f23d96/datashader-0.18.2-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/87/45/ca760deab4de448e6c0e3860fc187bcc49216eabda379f6ce68065158843/distributed-2025.12.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/ad/14/0fe5889a83991ac29c93e6b2e121ad2afc3bff5f9327f34447d3068d8142/distributed-2026.1.2-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/1a/7b/adf3f611f11997fc429d4b00a730604b65d952417f36a10c4be6e38e064d/duckdb-1.4.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/ee/25/a484c2ab78e9da027c88b7bad10c47b02655ba0ff31bab9ebb34c9866292/earthengine_api-1.7.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/a3/cf/7feb3222d770566ca9eaf0bf6922745fadd1ed7ab11832520063a515c240/ecmwf_datastores_client-0.4.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/53/32/256df3dbaa198c58539ad94f9a41e98c2c8ff23f126b8f5f52c7dcd0a738/duckdb-1.4.4-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/a7/39/ce46ee84779ef19d88fd028fc786a6dcc68b73ace33c31997aeda0dfecdc/earthengine_api-1.7.12-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/04/40/2ccf4c87a5f9c8198fe71600d5f307f5dada201c091af8774a9c1e360865/ecmwf_datastores_client-0.4.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/65/54/5e3b0e41799e17e5eff1547fda4aab53878c0adb4243de6b95f8ddef899e/ee_extra-2025.7.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/06/98/3e22f4386f6c1957f5994c9aa9cedd8a442bb75766bd0b2e2c1c92854af9/eemont-2025.7.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fa/09/f92f3d87c967d80fb73fa45a7b8ce6048fcf6bc9ba04ef0fb04443e209d3/eerepr-0.1.2-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/90/04/4a730d74fd908daad86d6b313f235cdf8e0cf1c255b392b7174ff63ea81a/einx-0.3.0-py3-none-any.whl
- - pypi: git+ssh://git@forgejo.tobiashoelzer.de:22222/tobias/entropy.git#9ca1bdf4afc4ac9b0ea29ebbc060ffecb5cffcf7
+ - pypi: git+ssh://git@forgejo.tobiashoelzer.de:22222/tobias/entropy.git#9152653278559faff830ff984a66d30b8ae5657c
+ - pypi: https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/7e/31/d229f6cdb9cbe03020499d69c4b431b705aa19a55aa0fe698c98022b2fef/faiss_cpu-1.12.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/c7/7d/74dd43d58f37584b32f0d781c8dbea9a286ee73e90393394e70569d4f254/fastai-2.8.6-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/84/f8/78832c84a957a01a332612ea6ee5a34f63690737000bf4f90995dcca367d/fastcore-1.11.3-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/23/03/2fe18e3d718b5a36d6c548df3e7662a4c433efea4d28662063d259248a1d/fastcore-1.12.11-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/47/60/ed35253a05a70b63e4f52df1daa39a6a464a3e22b0bd060b77f63e2e2b6a/fastdownload-0.0.7-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/61/48/895a29947b67e9b2da92b6370d519741ca7680ea8cf6c5f42bd887241984/fastlite-0.2.3-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fe/a7/af33584fa6d17b911cfaba460efd3409cb5dd47083c181a4fdfec4bef840/fastlite-0.2.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/79/45/4aa502bbda9b63c792463c3466a2c5ef3c0830935f81906043f66b2b6c74/fastprogress-1.1.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/47/3d/4b85b47a7e70d5c7cc0cf7d7b2883646c9c0bd3ef54a33f23d5873aa910c/fasttransform-0.0.2-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/ad/ec/2eff1f7617979121a38841b9c5b4fe0eaa64dc1d976cf4c85328a288ba8c/flox-0.10.8-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fa/97/3702c3be0e5ad3f46a75ccb9f30b6d20bd9432d9940a0c62dfa4869b4758/flox-0.11.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b5/a8/5f764f333204db0390362a4356d03a43626997f26818a0e9396f1b3bd8c9/folium-0.20.0-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/38/74/f94141b38a51a553efef7f510fc213894161ae49b88bffd037f8d2a7cb2f/frozendict-2.4.7-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/0f/4f/16e34c39f1840203216a79084d92aed6722ba00d34815807bc3e04d58c9f/geemap-0.36.6-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/82/f4/252ef20702aef20cf4514368918bede465ec4a7cb1ab8c1a647b9264cf61/geemap-0.37.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a9/fb/c1e92716ee5aa00d48b650f0cb43220a1bf4088c8d572dfc21d400b16723/geoarrow_rust_core-0.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/4f/6b/13166c909ad2f2d76b929a4227c952630ebaf0d729f6317eb09cbceccbab/geocoder-1.38.1-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/31/c6/a9341239e2e2953537b9e90a46ebc59f2e122247a3fe22373cc37520fc44/geocube-0.7.1-py3-none-any.whl
@@ -343,46 +347,48 @@ environments:
- pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/96/58/c1e716be1b055b504d80db2c8413f6c6a890a6ae218a65f178b63bc30356/google_api_python_client-2.187.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/04/44/3677ff27998214f2fa7957359da48da378a0ffff1bd0bdaba42e752bc13e/google_api_python_client-2.189.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/99/d5/3c97526c8796d3caf5f4b3bed2b05e8a7102326f00a334e7a438237f3b22/google_auth_httplib2-0.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/2d/80/6e5c7c83cea15ed4dfc4843b9df9db0716bc551ac938f7b5dd18a72bd5e4/google_cloud_storage-3.7.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/46/0b/816a6ae3c9fd096937d2e5f9670558908811d57d59ddf69dd4b83b326fd1/google_cloud_storage-3.9.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/0f/d6/0da119f5fc37311b34f301e1ef60f717bf9aa289f4fed9075bf4cced0406/h3-4.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/45/a6/9cac4b5ea1dad5767a1b6dcc2eb8255a5bc5ec2c968e558484fcde328304/h3-4.4.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/b6/08/b349ae3b7051b37155f40e09852193fc56f9aafe2edf6ef3e190eb329a2f/h3ronpy-0.22.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/d6/49/1f35189c1ca136b2f041b72402f2eb718bdcb435d9e88729fe6f6909c45d/h5netcdf-1.7.3-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/b1/8b/88f16936a8e8070a83d36239555227ecd91728f9ef222c5382cda07e0fd6/h5netcdf-1.8.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/8c/a2/0d269db0f6163be503775dc8b6a6fa15820cc9fdc866f6ba608d86b721f2/httplib2-0.31.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b6/cd/5b3334d39276067f54618ce0d0b48ed69d91352fbf137468c7095170d0e5/hyperopt-0.2.7-py2.py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/8c/d7/db466e07a21553441adbf915f0913a3f8fecece364cacb2392f11be267be/icechunk-1.1.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/1c/71/043ec2eb964d871f916ee949f99b71dbbd31ffe85c5a280edaaa6f877789/icechunk-1.1.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/59/81/d967317829a265c3a25c69791f7d2c8ce529d4bbcb1d8312a3cd1c6d2886/imagecodecs-2026.1.1-cp311-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/02/e1/8a299b91a4abee7c299c0c6625f9c0985c623fd4b6b41b5a5fe92508bb18/imagecodecs-2026.1.14-cp311-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/e8/10/530b51352d3dcbce5f242e9d9c07a4ece5a983af5d4cceb8c6fad4e8e087/imodels-2.0.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/2e/6e/3fa71d85753438dc926ba582194a6fc4139d213b4a7ed72741962020110d/interpret_core-0.7.5-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4c/0f/b66d63d4a5426c09005d3713b056e634e00e69788fdc88d1ffe40e5b7654/ipycytoscape-1.3.3-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/ca/d3/642a6dc3db8ea558a9b5fbc83815b197861868dc98f98a789b85c7660670/ipyevents-2.0.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/00/60/249e3444fcd9c833704741769981cd02fe2c7ce94126b1394e7a3b26e543/ipyfilechooser-0.6.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/49/69/e9858f2c0b99bf9f036348d1c84b8026f438bb6875effe6a9bcd9883dada/ipyleaflet-0.20.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl
@@ -391,6 +397,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/42/86/dabda8fbcb1b00bcfb0003c3776e8ade1aa7b413dff0a2c08f457dace22f/lightgbm-4.6.0-py3-none-manylinux_2_28_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/de/73/3d757cb3fc16f0f9794dd289bcd0c4a031d9cf54d8137d6b984b2d02edf3/lightning_utilities-0.15.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/c4/bd/ba44a47578ea48ee28b54543c1de8c529eedad8317516a2a753e6d9c77c5/lonboard-0.13.0-py3-none-any.whl
@@ -400,23 +407,24 @@ environments:
- pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/52/40/617b15e62d5de1718e81ee436a1f19d4d40274ead97ac0eda188baebb986/memray-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/b2/d6/de0cc74f8d36976aeca0dd2e9cbf711882ff8e177495115fd82459afdc4d/mercantile-1.2.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/4c/43/2fc7f76c8891aef148901f1ba3dee65c1cbac00a85ae5ee0dabc2b861256/mlxtend-0.23.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/51/c0/00c9809d8b9346eb238a6bbd5f83e846a4ce4503da94a4c08cb7284c325b/multipledispatch-1.0.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/93/cf/be4e93afbfa0def2cd6fac9302071db0bd6d0617999ecbf53f92b9398de3/multiurl-0.3.7-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/e2/63/58e2de2b5232cd294c64092688c422196e74f9fa8b3958bdf02d33df24b9/murmurhash-1.0.15-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/7b/7a/a8d32501bb95ecff342004a674720164f95ad616f269450b3bc13dc88ae3/netcdf4-1.7.4-cp311-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/ae/d3/ff8f1b9968aa4dcd1da1880322ed492314cc920998182e549b586c895a17/numbagg-0.9.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/c4/e6/d359fdd37498e74d26a167f7a51e54542e642ea47181eb4e643a69a066c3/numcodecs-0.16.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/b0/e0/760e73c111193db5ca37712a148e4807d1b0c60302ab31e4ada6528ca34d/numpy_groupies-0.11.3-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/23/2d/609d0392d992259c6dc39881688a7fc13b1397a668bc360fbd68d1396f85/nvidia_nccl_cu12-2.29.2-py3-none-manylinux_2_18_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/31/5a/cac7d231f322b66caa16fd4b136ebc8e4b18b2805811c2d58dc47210cdea/nvidia_nccl_cu12-2.29.3-py3-none-manylinux_2_18_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/53/20/08c6dc0f20c1394e2324b9344838e4e7af770cdcb52c30757a475f50daeb/obstore-0.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/99/e2/311fb383d9534eef7bfbe858fad931b6e3dbe85843c50592f50063c3bc83/odc_geo-0.4.10-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/92/58/5148ae95713c84255267b26eddfe1ac926244aca4327bdeb4cf2d6d576bf/odc_loader-0.6.2-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/e2/c7/b8f2b3e53f26f8f463002f3e8023189653b627b22ba6c00ef86eaba50b73/odc_stac-0.5.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/8e/8f/22b0a79e458392e377a9784cc93e1bd6aaa3f908d52969a81e0c56e6b59d/odc_loader-0.6.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/01/8a/fd93f13a9b4b688b5ac8c8ec14ed6401db99166b7d55c41c9c96250398d9/odc_stac-0.5.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b5/ed/9fbdeb23a09e430d87b7d72d430484b88184633dc50f6bfb792354b6f661/opencensus-0.11.4-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/10/68/162c97ea78c957d68ecf78a5c5041d2e25bd5562bdf5d89a6cbf7f8429bf/opencensus_context-0.1.3-py2.py3-none-any.whl
@@ -426,70 +434,75 @@ environments:
- pypi: https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/64/20/69f2a39792a653fd64d916cd563ed79ec6e5dcfa6408c4674021d810afcf/pandas_stubs-2.3.3.251219-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/11/da/9d476e9aadfa854719f3cb917e3f7a170a657a182d8d1d6e546594a4872b/param-2.3.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d1/c6/df1fe324248424f77b89371116dab5243db7f052c32cc9fe7442ad9c5f75/pandas_stubs-2.3.3.260113-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/11/b6/f8c7e1f5f716e16070cf35f90c24f95f397376bb810e65000b6bc55950cc/param-2.3.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/c0/db/61efa0d08a99f897ef98256b03e563092d36cc38dc4ebe4a85020fe40b31/pbr-7.0.3-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/e9/8e/24e0bb90b2d75af84820693260c5534e9ed351afdda67ed6f393a141a0e2/plotly-6.5.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/8a/67/f95b5460f127840310d2187f916cf0023b5875c0717fdf893f71e1325e87/plotly-6.5.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/42/88/71fa06eb487ed9d4fab0ad173300b7a58706385f98fb66b1ccdc3ec3d4dd/plum_dispatch-2.6.1-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/a7/4b/e4759803793843ce2258ddef3b3a744bdf0318c77c3ac10560683a2eee60/posthog-6.9.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/17/73/f388398f8d789f69b510272d144a9186d658423f6d3ecc484c0fe392acec/preshed-3.0.12-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/38/8c/6522b8e543ece46f645911c3cebe361d8460134c0fee02ddcf70ebf32999/protobuf-6.33.3-cp39-abi3-manylinux2014_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/2d/4f/3593e5adb88a188c798604aed95fbc1479f30230e7f51e8f2c770e6a3832/psygnal-0.15.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/bd/db/ea0203e495be491c85af87b66e37acfd3bf756fd985f87e46fc5e3bf022c/py4j-0.10.9.9-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/68/fb/bc7f639aed026bca6e7beb1e33f6951e16b7d315594e7635a4f7d21d63f4/py_spy-0.4.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/8c/b2/23f4032cd1c9744aa8e9ecda43cd4d755fcb209f7f40fae035248f31a679/pyct-0.6.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/e5/e0/050018d855d26d3c0b4a7d1b2ed692be758ce276d8289e2a2b44ba1014a5/pyerfa-2.0.1.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/46/35/b874f79d03e9f900012cf609f7fff97b77164f2e14ee5aac282f8a999c1b/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/91/fb/3380832944eb4552b5873dd8c75095250356edfadf1156bd562da04fc793/pypalettes-0.2.1-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/82/06/cad54e8ce758bd836ee5411691cbd49efeb9cc611b374670fce299519334/pyshp-3.0.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/ad/b4/a9430e72bfc3c458e1fcf8363890994e483052ab052ed93912be4e5b32c8/pystac-1.14.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5d/d2/5f6367b14c9f250d1a6725d18bd1e9584f5ab1587e292f3a847e59189598/pystac_client-0.9.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/0d/48/75c0c38ea2b086cd967b7241f394773437a1a7c09dc9b178b49f04399207/pytabkit-1.7.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/88/ae/baf3a8057d8129896a7e02619df43ea0d918fc5b2bb66eb6e2470595fbac/python_box-7.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/f7/1b/354a0ab669703f87e9ab0464670be8791a9de59c2693ffb0d9a584927b5e/python_fasthtml-0.12.39-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/22/da/e890190fd9b3bd465c796a341fb24cbb1b0f9d831f511d21db9faf056e32/python_fasthtml-0.12.41-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/0e/93/c8c361bf0a2fe50f828f32def460e8b8a14b93955d3fd302b1a9b63b19e4/pytorch_lightning-2.6.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/05/19/94d6c66184c7d0f9374330c714f62c147dbb53eda9efdcc8fc6e2ac454c5/rasterio-1.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/f2/98/7e6d147fd16a10a5f821db6e25f192265d6ecca3d82957a4fdd592cad49c/ratelim-0.1.6-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a0/a0/b5e0099e1b1b3dc2e4c6c78a6630fd97ed2706cd47daba4d7872897cfe86/ray-2.52.1-cp313-cp313-manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/d6/e5/4f4fc949e7eb8415a57091767969e1d314dcf06b74b85bbbf29991395af4/rioxarray-0.20.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/62/aa/4ba81b24acf2b05cb00fd7603f84e5088af72596839f85c23c6ff5d249d7/rioxarray-0.21.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/44/8c/04797ebb53748b4d594d4c334b2d9a99f2d2e06e19ad505f1313ca5d56eb/s3fs-2025.12.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5f/e1/5ef25f52973aa12a19cf4e1375d00932d7fb354ffd310487ba7d44225c1a/s3transfer-0.15.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/a3/bb/bbae36d06c0fd670e8373da67096cd57058b57c9bad7d92969b5e3b730af/scooby-0.11.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/ca/63/2c6daf59d86b1c30600bff679d039f57fd1932af82c43c0bde1cbc55e8d4/sentry_sdk-2.52.0-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/6b/6a/c006de5df0e0f4850aa94019df1f79bf6a5342fa851ca85e4728691fd0c4/shap-0.50.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/63/81/9ef641ff4e12cbcca30e54e72fb0951a2ba195d0cda0ba4100e532d929db/slicer-0.0.8-py3-none-any.whl
@@ -497,7 +510,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/ad/95/bc978be7ea0babf2fb48a414b6afaad414c6a9e8b1eafc5b8a53c030381a/smart_open-7.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/c6/1f/731beb48f2c7415a71e2f655876fea8a0b3a6798be3d4d51b794f939623d/spacy-3.8.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/c3/55/12e842c70ff8828e34e543a2c7176dac4da006ca6901c9e8b43efab8bc6b/spacy_legacy-3.0.12-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/33/78/d1a1a026ef3af911159398c939b1509d5c36fe524c7b644f34a5146c4e16/spacy_loggers-1.0.5-py3-none-any.whl
@@ -505,31 +518,36 @@ environments:
- pypi: https://files.pythonhosted.org/packages/c3/2f/66044ef5a10a487652913c1a7f32396cb0e9e32ecfc3fdc0a0bc0382e703/srsly-2.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/30/09/cd7134f1ed5074a7d456640e7ba9a8c8e68a831837b4e7bfd9f29e5700a4/st_theme-1.2.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/18/c4/09985a03dba389d4fe16a9014147a7b02fa76ef3519bf5846462a485876d/starlette-0.51.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/f7/45/8c4ebc0c460e6ec38e62ab245ad3c7fc10b210116cea7c16d61602aa9558/stevedore-5.4.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/7a/31/7d601cc639b0362a213552a838af601105591598a4b08ec80666458083d2/stopuhr-0.0.10-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/c0/95/6b7873f0267973ebd55ba9cd33a690b35a116f2779901ef6185a0e21864d/streamlit-1.52.2-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/48/1d/40de1819374b4f0507411a60f4d2de0d620a9b10c817de5925799132b6c9/streamlit-1.54.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/72/35/d3cdab8cff94971714f866181abb1aa84ad976f6e7b6218a0499197465e4/streamlit_folium-0.25.3-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/f0/ec/23e60c7c3f4d5247cac66a8d79f2f0070f4ef90f11570ccdb00e4d69da05/tabdpt-1.1.12-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/c9/56/bbac0d5076acbca1075177c0a40c829a497fc26598fc90fa55c73d38288a/tabicl-0.1.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/a8/db/db2e7c3cd98499f313ab59021a01124f37f01d08a96daf563e803cfd5ab7/tabpfn-6.2.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/34/63/67c2453f4a30a969750c5c4296caa3caaf07e363dee617f316d1380ee3d0/tabpfn_common_utils-0.2.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/e0/1d/b5d63f1a6b824282b57f7b581810d20b7a28ca951f2d5b59f1eb0782c12b/tensorboardx-2.6.4-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/a9/f4/48e4a4c77ab7eea48d3b0a77f8dea0be101c83421abc64da0888c77c47cf/textual-7.1.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/9c/78/96ddb99933e11d91bc6e05edae23d2687e44213066bcbaca338898c73c47/textual-7.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/45/ef/e7fca88074cb0aa1c1a23195470b4549492c2797fe7dc9ff79a85500153a/thinc-8.3.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/1b/fe/e59859aa1134fac065d36864752daf13215c98b379cb5d93f954dc0ec830/tifffile-2025.12.20-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/09/19/529b28ca338c5a88315e71e672badc85eef89460c248c4164f6ce058f8c7/tifffile-2026.1.28-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/02/21/aa0f434434c48490f91b65962b1ce863fdcce63febc166ca9fe9d706c2b6/torchmetrics-1.8.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/10/b5/5bba24ff9d325181508501ed7f0c3de8ed3dd2edca0784d48b144b6c5252/torchvision-0.24.1-cp313-cp313-manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/6a/6b/2f416568b3c4c91c96e5a365d164f8a4a4a88030aa8ab4644181fdadce97/transformers-4.57.3-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/74/18/8dd4fe6df1fd66f3e83b4798eddb1d8482d9d9b105f25099b76703402ebb/ty-0.0.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/e7/c1/56ef16bf5dcd255155cc736d276efa6ae0a5c26fd685e28f0412a4013c01/types_pytz-2025.2.0.20251108-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/43/6c/b26831b890b37c09882f6406efd31441c8e512bf1efbc967b9d867c5e02b/ultraplot-1.70.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d6/32/48209716f9715d77f1bce084ad74c5d3cfcf41fd78d0c7e7dbe4829cfa3a/ultraplot-1.72.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl
@@ -537,17 +555,18 @@ environments:
- pypi: https://files.pythonhosted.org/packages/e6/9f/ca52771fe972e0dcc5167fedb609940e01516066938ff2ee28b273ae4f29/vega_datasets-0.9.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/04/d5/81d1403788f072e7d0e2b2fe539a0ae4410f27886ff52df094e5348c99ea/vegafusion-2.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl
- - pypi: https://files.pythonhosted.org/packages/6f/61/dc6f4a38cf1b8699f64c57d7f021ca42c39bfe782d8a6eaefb7e8418e925/vl_convert_python-1.9.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/a0/18/88e02899b72fa8273ffb32bde12b0e5776ee0fd9fb29559a49c48ec4c5fa/vl_convert_python-1.9.0.post1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+ - pypi: https://files.pythonhosted.org/packages/70/c8/1b758bd903afee000f023cd03f335ff328a21b3914f9f9deda49b1e57723/wandb-0.24.2-py3-none-manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a4/74/a148b41572656904a39dfcfed3f84dd1066014eed94e209223ae8e9d088d/weasel-0.4.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
- pypi: git+https://github.com/davbyr/xAnimate#750e03e480db309407e09f4ffe5f49522a4c4f9b
- - pypi: https://files.pythonhosted.org/packages/d5/e4/62a677feefde05b12a70a4fc9bdc8558010182a801fbcab68cb56c2b0986/xarray-2025.12.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/86/b4/cfa7aa56807dd2d9db0576c3440b3acd51bae6207338ec5610d4878e5c9b/xarray-2025.11.0-py3-none-any.whl
- pypi: git+https://github.com/relativityhd/xarray-spatial#3a3120981dc910cbfc824bd03d1c1f8637efaf2d
- pypi: https://files.pythonhosted.org/packages/14/38/d1a8b0c8b7749fde76daa12ec3e63aa052cf37cacc2e9715377ce0197a99/xarray_histogram-0.2.2-py3-none-any.whl
- pypi: git+https://github.com/relativityhd/xdggs?branch=feature%2Fmake-plotting-useful#d85688e638350804dac37d05725709a9c909e5be
@@ -901,10 +920,10 @@ packages:
- astropy[dev] ; extra == 'dev-all'
- astropy[test-all] ; extra == 'dev-all'
requires_python: '>=3.11'
-- pypi: https://files.pythonhosted.org/packages/57/61/2d06c08f022c9b617b79f6c55d88e596c1795a1d211e6bf584ac4b9e9506/astropy_iers_data-0.2026.1.5.0.43.43-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/74/51/59effa402d4ce8813e42eb62416059d42dd07826b0e7aa2db057c336972d/astropy_iers_data-0.2026.2.2.0.48.1-py3-none-any.whl
name: astropy-iers-data
- version: 0.2026.1.5.0.43.43
- sha256: fe2c35e9abc99142083d717ea76bf7bde373dc12e502aaeced28ae4ff9bfc345
+ version: 0.2026.2.2.0.48.1
+ sha256: 62aecb2faea740e0d714808b85512ebe4f29adbfe1e8d5e5481cfd66494d164f
requires_dist:
- pytest ; extra == 'docs'
- hypothesis ; extra == 'test'
@@ -1276,10 +1295,10 @@ packages:
purls: []
size: 3472674
timestamp: 1765257107074
-- pypi: https://files.pythonhosted.org/packages/ee/34/a9914e676971a13d6cc671b1ed172f9804b50a3a80a143ff196e52f4c7ee/azure_core-1.37.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl
name: azure-core
- version: 1.37.0
- sha256: b3abe2c59e7d6bb18b38c275a5029ff80f98990e7c90a5e646249a56630fcc19
+ version: 1.38.0
+ sha256: ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335
requires_dist:
- requests>=2.21.0
- typing-extensions>=4.6.0
@@ -1370,6 +1389,11 @@ packages:
purls: []
size: 299198
timestamp: 1761094654852
+- pypi: https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl
+ name: backoff
+ version: 2.2.1
+ sha256: 63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8
+ requires_python: '>=3.7,<4.0'
- pypi: https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl
name: beartype
version: 0.22.9
@@ -1517,13 +1541,13 @@ packages:
- tornado>=6.2 ; sys_platform != 'emscripten'
- xyzservices>=2021.9.1
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/96/9a/663251dfb35aaddcbdbef78802ea5a9d3fad9d5fadde8774eacd9e1bfbb7/boost_histogram-1.6.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/9b/fa/a8198c3f4cb4a4989394383590dcdbaaad0be1660e8800016cbb7bd91f08/boost_histogram-1.7.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
name: boost-histogram
- version: 1.6.1
- sha256: c5700e53bf2d3d006f71610f71fc592d88dab3279dad300e8178dc084018b177
+ version: 1.7.1
+ sha256: 624da520a10ad1e44620055b9ff5c806fc1c961affe3340133105fa6368ed3be
requires_dist:
- - numpy
- requires_python: '>=3.9'
+ - numpy>=1.21.3
+ requires_python: '>=3.10'
- pypi: https://files.pythonhosted.org/packages/3c/56/f47a80254ed4991cce9a2f6d8ae8aafbc8df1c3270e966b2927289e5a12f/boto3-1.41.5-py3-none-any.whl
name: boto3
version: 1.41.5
@@ -1674,10 +1698,10 @@ packages:
- requests>=2.5.0
- tqdm
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/27/27/6414b1b7e5e151300c54e28ad1cf3e3b34fe66dc3256a989b031166b1ba3/cdshealpix-0.7.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/ce/0c/00c768197bbcba5835f22b69c2611cb422c0f8a9dc93af62e26d14584b29/cdshealpix-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
name: cdshealpix
- version: 0.7.2
- sha256: 55ec0d3733b0908968db5ed879718317602f00bf60cfaeebd2aa8e647a3b37b7
+ version: 0.8.0
+ sha256: 424da709a0a88aef5e0ce52b4e4195081418d6fd8b28e751dd7ee9f8d2adf617
requires_dist:
- astropy<5.3 ; python_full_version == '3.8.*'
- astropy ; python_full_version >= '3.9'
@@ -1688,10 +1712,10 @@ packages:
version: 2026.1.4
sha256: 9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c
requires_python: '>=3.7'
-- pypi: https://files.pythonhosted.org/packages/a3/8f/c42a98f933022c7de00142526c9b6b7429fdcd0fc66c952b4ebbf0ff3b7f/cf_xarray-0.10.10-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/65/ce/5c4f4660da5521d90bea62cdf8396d7e4ce4a00513e218d267b97f9ea453/cf_xarray-0.10.11-py3-none-any.whl
name: cf-xarray
- version: 0.10.10
- sha256: 04cbe8b2b5773849bda989059239ee7edddf5b00bf1783a8202e3b50e5f369d1
+ version: 0.10.11
+ sha256: c47fff625766c69a66fedef368d9787acb0819b32d8bd022f8b045089b42109a
requires_dist:
- xarray>=2024.7.0
- matplotlib ; extra == 'all'
@@ -1847,17 +1871,17 @@ packages:
purls: []
size: 48369
timestamp: 1765019689213
-- pypi: https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl
name: cryptography
- version: 46.0.3
- sha256: a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec
+ version: 46.0.4
+ sha256: 0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255
requires_dist:
- cffi>=1.14 ; python_full_version == '3.8.*' and platform_python_implementation != 'PyPy'
- cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy'
- typing-extensions>=4.13.2 ; python_full_version < '3.11'
- bcrypt>=3.1.5 ; extra == 'ssh'
- nox[uv]>=2024.4.15 ; extra == 'nox'
- - cryptography-vectors==46.0.3 ; extra == 'test'
+ - cryptography-vectors==46.0.4 ; extra == 'test'
- pytest>=7.4.0 ; extra == 'test'
- pytest-benchmark>=4.0 ; extra == 'test'
- pytest-cov>=2.10.1 ; extra == 'test'
@@ -2366,10 +2390,10 @@ packages:
- pytest-cov ; extra == 'tests'
- pytest-xdist ; extra == 'tests'
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/20/5b/0eceb9a5990de9025733a0d212ca43649ba9facd58b8552b6bf93c11439d/cyclopts-4.4.4-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/1c/7c/996760c30f1302704af57c66ff2d723f7d656d0d0b93563b5528a51484bb/cyclopts-4.5.1-py3-none-any.whl
name: cyclopts
- version: 4.4.4
- sha256: 316f798fe2f2a30cb70e7140cfde2a46617bfbb575d31bbfdc0b2410a447bd83
+ version: 4.5.1
+ sha256: 0642c93601e554ca6b7b9abd81093847ea4448b2616280f2a0952416574e8c7a
requires_dist:
- attrs>=23.1.0
- docstring-parser>=0.15,<4.0
@@ -2409,10 +2433,10 @@ packages:
version: 2.0.13
sha256: e96848faaafccc0abd631f1c5fb194eac0caee4f5a8777fdbb3e349d3a21741c
requires_python: '>=3.9,<3.15'
-- pypi: https://files.pythonhosted.org/packages/6f/3a/2121294941227c548d4b5f897a8a1b5f4c44a58f5437f239e6b86511d78e/dask-2025.12.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/e5/23/d39ccc4ed76222db31530b0a7d38876fdb7673e23f838e8d8f0ed4651a4f/dask-2026.1.2-py3-none-any.whl
name: dask
- version: 2025.12.0
- sha256: 4213ce9c5d51d6d89337cff69de35d902aa0bf6abdb8a25c942a4d0281f3a598
+ version: 2026.1.2
+ sha256: 46a0cf3b8d87f78a3d2e6b145aea4418a6d6d606fe6a16c79bd8ca2bb862bc91
requires_dist:
- click>=8.1
- cloudpickle>=3.0.0
@@ -2425,12 +2449,12 @@ packages:
- numpy>=1.24 ; extra == 'array'
- dask[array] ; extra == 'dataframe'
- pandas>=2.0 ; extra == 'dataframe'
- - pyarrow>=14.0.1 ; extra == 'dataframe'
- - distributed>=2025.12.0,<2025.12.1 ; extra == 'distributed'
+ - pyarrow>=16.0 ; extra == 'dataframe'
+ - distributed>=2026.1.2,<2026.1.3 ; extra == 'distributed'
- bokeh>=3.1.0 ; extra == 'diagnostics'
- jinja2>=2.10.3 ; extra == 'diagnostics'
- dask[array,dataframe,diagnostics,distributed] ; extra == 'complete'
- - pyarrow>=14.0.1 ; extra == 'complete'
+ - pyarrow>=16.0 ; extra == 'complete'
- lz4>=4.3.2 ; extra == 'complete'
- pandas[test] ; extra == 'test'
- pytest ; extra == 'test'
@@ -2460,10 +2484,10 @@ packages:
- xarray
- pytest ; extra == 'tests'
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl
name: debugpy
- version: 1.8.19
- sha256: 360ffd231a780abbc414ba0f005dad409e71c78637efe8f2bd75837132a41d38
+ version: 1.8.20
+ sha256: 5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7
requires_python: '>=3.8'
- pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl
name: decorator
@@ -2474,14 +2498,14 @@ packages:
name: distlib
version: 0.4.0
sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16
-- pypi: https://files.pythonhosted.org/packages/87/45/ca760deab4de448e6c0e3860fc187bcc49216eabda379f6ce68065158843/distributed-2025.12.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/ad/14/0fe5889a83991ac29c93e6b2e121ad2afc3bff5f9327f34447d3068d8142/distributed-2026.1.2-py3-none-any.whl
name: distributed
- version: 2025.12.0
- sha256: 35d18449002ea191e97f7e04a33e16f90c2243486be52d4d0f991072ea06b48a
+ version: 2026.1.2
+ sha256: 30ccb5587351f50304f6f6e219ea91bc09d88401125779caa8be5253e9d3ecf2
requires_dist:
- click>=8.0
- cloudpickle>=3.0.0
- - dask>=2025.12.0,<2025.12.1
+ - dask>=2026.1.2,<2026.1.3
- jinja2>=2.10.3
- locket>=1.0.0
- msgpack>=1.0.2
@@ -2495,6 +2519,11 @@ packages:
- urllib3>=1.26.5
- zict>=3.0.0
requires_python: '>=3.10'
+- pypi: https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl
+ name: distro
+ version: 1.9.0
+ sha256: 7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
+ requires_python: '>=3.6'
- conda: https://conda.anaconda.org/conda-forge/linux-64/dlpack-0.8-h59595ed_3.conda
sha256: 5884a5e18a779586a2179c0187ef75caa148568dd576c4dbc278deead77cf4b1
md5: ee290dba0b7497f2d357c057aaea123f
@@ -2535,10 +2564,10 @@ packages:
- pytest ; extra == 'test'
- cloudpickle ; extra == 'test'
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/1a/7b/adf3f611f11997fc429d4b00a730604b65d952417f36a10c4be6e38e064d/duckdb-1.4.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/53/32/256df3dbaa198c58539ad94f9a41e98c2c8ff23f126b8f5f52c7dcd0a738/duckdb-1.4.4-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
name: duckdb
- version: 1.4.3
- sha256: a2813f4635f4d6681cc3304020374c46aca82758c6740d7edbc237fe3aae2744
+ version: 1.4.4
+ sha256: 7df7351328ffb812a4a289732f500d621e7de9942a3a2c9b6d4afcf4c0e72526
requires_dist:
- ipython ; extra == 'all'
- fsspec ; extra == 'all'
@@ -2547,10 +2576,10 @@ packages:
- pyarrow ; extra == 'all'
- adbc-driver-manager ; extra == 'all'
requires_python: '>=3.9.0'
-- pypi: https://files.pythonhosted.org/packages/ee/25/a484c2ab78e9da027c88b7bad10c47b02655ba0ff31bab9ebb34c9866292/earthengine_api-1.7.4-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/a7/39/ce46ee84779ef19d88fd028fc786a6dcc68b73ace33c31997aeda0dfecdc/earthengine_api-1.7.12-py3-none-any.whl
name: earthengine-api
- version: 1.7.4
- sha256: ed785d59a88c24abcc219853afa9c4295989e28f34685063504389c3fff1e983
+ version: 1.7.12
+ sha256: 39c24f65b97e88bfed325e55d7f9fa5c8e8a9f92280c5ed24e3ab36560f2b543
requires_dist:
- google-cloud-storage
- google-api-python-client>=1.12.1
@@ -2562,10 +2591,10 @@ packages:
- geopandas ; extra == 'tests'
- numpy ; extra == 'tests'
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/a3/cf/7feb3222d770566ca9eaf0bf6922745fadd1ed7ab11832520063a515c240/ecmwf_datastores_client-0.4.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/04/40/2ccf4c87a5f9c8198fe71600d5f307f5dada201c091af8774a9c1e360865/ecmwf_datastores_client-0.4.2-py3-none-any.whl
name: ecmwf-datastores-client
- version: 0.4.1
- sha256: a14515a892889a602ec2c81b15ec81d048932ca05480f12c778136028b6230a3
+ version: 0.4.2
+ sha256: d22a675b35263286de09969502ec897da9ceb9e4c8ec4d709f7ebb3b90d3ae98
requires_dist:
- attrs
- multiurl>=0.3.7
@@ -2600,11 +2629,11 @@ packages:
requires_dist:
- earthengine-api
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl
name: einops
- version: 0.8.1
- sha256: 919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737
- requires_python: '>=3.8'
+ version: 0.8.2
+ sha256: 54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193
+ requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/90/04/4a730d74fd908daad86d6b313f235cdf8e0cf1c255b392b7174ff63ea81a/einx-0.3.0-py3-none-any.whl
name: einx
version: 0.3.0
@@ -2619,7 +2648,7 @@ packages:
- pypi: ./
name: entropice
version: 0.1.0
- sha256: 879d02081ed946a95d41ff65c226e0e5a053f288fd930553215b2ae4304d8df6
+ sha256: 07232c2b09b1b8b691cc8ca7d25b3c0041f2324236a11491f9f07b7e6827973a
requires_dist:
- aiohttp>=3.12.11
- bokeh>=3.7.3
@@ -2682,10 +2711,12 @@ packages:
- ruff>=0.14.11,<0.15
- pandas-stubs>=2.3.3.251201,<3
- pytest>=9.0.2,<10
- - autogluon-tabular[all,mitra]>=1.5.0
+ - autogluon-tabular[all,mitra,realmlp,interpret,fastai,tabm,tabpfn,tabdpt,tabpfnmix,tabicl,skew,imodels]>=1.5.0
- shap>=0.50.0,<0.51
+ - h5py>=3.15.1,<4
+ - pydantic>=2.12.5,<3
requires_python: '>=3.13,<3.14'
-- pypi: git+ssh://git@forgejo.tobiashoelzer.de:22222/tobias/entropy.git#9ca1bdf4afc4ac9b0ea29ebbc060ffecb5cffcf7
+- pypi: git+ssh://git@forgejo.tobiashoelzer.de:22222/tobias/entropy.git#9152653278559faff830ff984a66d30b8ae5657c
name: entropy
version: 0.1.0
requires_dist:
@@ -2694,6 +2725,13 @@ packages:
- array-api-compat>=1.12.0,<2
- array-api-extra>=0.9.0,<0.10
requires_python: '>=3.11,<3.14'
+- pypi: https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl
+ name: eval-type-backport
+ version: 0.3.1
+ sha256: 279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8
+ requires_dist:
+ - pytest ; extra == 'tests'
+ requires_python: '>=3.7'
- pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl
name: executing
version: 2.2.1
@@ -2707,6 +2745,14 @@ packages:
- littleutils ; extra == 'tests'
- rich ; python_full_version >= '3.11' and extra == 'tests'
requires_python: '>=3.8'
+- pypi: https://files.pythonhosted.org/packages/7e/31/d229f6cdb9cbe03020499d69c4b431b705aa19a55aa0fe698c98022b2fef/faiss_cpu-1.12.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
+ name: faiss-cpu
+ version: 1.12.0
+ sha256: 4d57ed7aac048b18809af70350c31acc0fb9f00e6c03b6ed1651fd58b174882d
+ requires_dist:
+ - numpy>=1.25.0,<3.0
+ - packaging
+ requires_python: '>=3.9,<3.15'
- pypi: https://files.pythonhosted.org/packages/c7/7d/74dd43d58f37584b32f0d781c8dbea9a286ee73e90393394e70569d4f254/fastai-2.8.6-py3-none-any.whl
name: fastai
version: 2.8.6
@@ -2755,12 +2801,11 @@ packages:
- accelerate>=0.21 ; extra == 'dev'
- ipykernel ; extra == 'dev'
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/84/f8/78832c84a957a01a332612ea6ee5a34f63690737000bf4f90995dcca367d/fastcore-1.11.3-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/23/03/2fe18e3d718b5a36d6c548df3e7662a4c433efea4d28662063d259248a1d/fastcore-1.12.11-py3-none-any.whl
name: fastcore
- version: 1.11.3
- sha256: 998825f8f69c44ab16a156bb60e9a6d0988f3e8aa5b2459ec95289b520cf1c0b
+ version: 1.12.11
+ sha256: b6a0ce9f48509405109251d00ac0576cfe5cba0a2b1b495a4126283969efbad5
requires_dist:
- - packaging
- numpy ; extra == 'dev'
- nbdev>=0.2.39 ; extra == 'dev'
- matplotlib ; extra == 'dev'
@@ -2781,10 +2826,10 @@ packages:
- fastprogress
- fastcore>=1.3.26
requires_python: '>=3.6'
-- pypi: https://files.pythonhosted.org/packages/61/48/895a29947b67e9b2da92b6370d519741ca7680ea8cf6c5f42bd887241984/fastlite-0.2.3-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/fe/a7/af33584fa6d17b911cfaba460efd3409cb5dd47083c181a4fdfec4bef840/fastlite-0.2.4-py3-none-any.whl
name: fastlite
- version: 0.2.3
- sha256: 0ebc1feaa728165835dc6f2b82521889929bcee2ce1e62287b17a5cfd19a1022
+ version: 0.2.4
+ sha256: 869d96791b06535845b42f7ddef6e12f8e14f6b120f96b9701a4f16867189c63
requires_dist:
- fastcore>=1.7.1
- apswutils>=0.1.2
@@ -2834,10 +2879,10 @@ packages:
- pkg:pypi/filelock?source=hash-mapping
size: 18609
timestamp: 1765846639623
-- pypi: https://files.pythonhosted.org/packages/ad/ec/2eff1f7617979121a38841b9c5b4fe0eaa64dc1d976cf4c85328a288ba8c/flox-0.10.8-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/fa/97/3702c3be0e5ad3f46a75ccb9f30b6d20bd9432d9940a0c62dfa4869b4758/flox-0.11.0-py3-none-any.whl
name: flox
- version: 0.10.8
- sha256: 9c5e6dc45717aab74d8a79b64e7c4224ea7fa40fbdefe37290bd6be1171dc581
+ version: 0.11.0
+ sha256: 61620abc0eec12a3d6f93fd08f17326435b17d256678a5380598d10b25012751
requires_dist:
- pandas>=2.1
- packaging>=21.3
@@ -2953,10 +2998,10 @@ packages:
version: 1.0.0
sha256: 929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216
requires_python: '>=2.6,!=3.0.*,!=3.1.*,!=3.2.*'
-- pypi: https://files.pythonhosted.org/packages/0f/4f/16e34c39f1840203216a79084d92aed6722ba00d34815807bc3e04d58c9f/geemap-0.36.6-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/82/f4/252ef20702aef20cf4514368918bede465ec4a7cb1ab8c1a647b9264cf61/geemap-0.37.1-py3-none-any.whl
name: geemap
- version: 0.36.6
- sha256: bdbaa610ea39a123bc841b43713e4a9f73fb2cd2bf30e7acb6b1a0215b7d786a
+ version: 0.37.1
+ sha256: b3df7999e94cf10038a3c66e7c6e6b8415421d7ab8ce8a2c4928e18d8dad6293
requires_dist:
- anywidget
- bqplot
@@ -2974,6 +3019,7 @@ packages:
- pyperclip
- pyshp>=2.3.1
- python-box
+ - requests
- scooby
- google-api-core ; extra == 'ai'
- google-cloud-aiplatform ; extra == 'ai'
@@ -3090,13 +3136,14 @@ packages:
- rioxarray ; extra == 'raster'
- psycopg2 ; extra == 'sql'
- sqlalchemy ; extra == 'sql'
+ - psutil ; extra == 'tests'
- geopandas ; extra == 'vector'
- osmnx ; extra == 'vector'
- ffmpeg-python ; extra == 'workshop'
- gdown ; extra == 'workshop'
- geedim ; extra == 'workshop'
- geopandas ; extra == 'workshop'
- requires_python: '>=3.9'
+ requires_python: '>=3.10'
- pypi: https://files.pythonhosted.org/packages/a9/fb/c1e92716ee5aa00d48b650f0cb43220a1bf4088c8d572dfc21d400b16723/geoarrow_rust_core-0.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
name: geoarrow-rust-core
version: 0.6.1
@@ -3296,10 +3343,10 @@ packages:
- grpcio-gcp>=0.2.2,<1.0.0 ; extra == 'grpcgcp'
- grpcio-gcp>=0.2.2,<1.0.0 ; extra == 'grpcio-gcp'
requires_python: '>=3.7'
-- pypi: https://files.pythonhosted.org/packages/96/58/c1e716be1b055b504d80db2c8413f6c6a890a6ae218a65f178b63bc30356/google_api_python_client-2.187.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/04/44/3677ff27998214f2fa7957359da48da378a0ffff1bd0bdaba42e752bc13e/google_api_python_client-2.189.0-py3-none-any.whl
name: google-api-python-client
- version: 2.187.0
- sha256: d8d0f6d85d7d1d10bdab32e642312ed572bdc98919f72f831b44b9a9cebba32f
+ version: 2.189.0
+ sha256: a258c09660a49c6159173f8bbece171278e917e104a11f0640b34751b79c8a1a
requires_dist:
- httplib2>=0.19.0,<1.0.0
- google-auth>=1.32.0,!=2.24.0,!=2.25.0,<3.0.0
@@ -3307,22 +3354,20 @@ packages:
- google-api-core>=1.31.5,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0
- uritemplate>=3.0.1,<5
requires_python: '>=3.7'
-- pypi: https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl
name: google-auth
- version: 2.47.0
- sha256: c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498
+ version: 2.48.0
+ sha256: 2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f
requires_dist:
- pyasn1-modules>=0.2.1
+ - cryptography>=38.0.3
- rsa>=3.1.4,<5
- cryptography>=38.0.3 ; extra == 'cryptography'
- aiohttp>=3.6.2,<4.0.0 ; extra == 'aiohttp'
- requests>=2.20.0,<3.0.0 ; extra == 'aiohttp'
- - cryptography ; extra == 'enterprise-cert'
- pyopenssl ; extra == 'enterprise-cert'
- pyopenssl>=20.0.0 ; extra == 'pyopenssl'
- - cryptography>=38.0.3 ; extra == 'pyopenssl'
- pyjwt>=2.0 ; extra == 'pyjwt'
- - cryptography>=38.0.3 ; extra == 'pyjwt'
- pyu2f>=0.1.5 ; extra == 'reauth'
- requests>=2.20.0,<3.0.0 ; extra == 'requests'
- grpcio ; extra == 'testing'
@@ -3330,12 +3375,10 @@ packages:
- freezegun ; extra == 'testing'
- oauth2client ; extra == 'testing'
- pyjwt>=2.0 ; extra == 'testing'
- - cryptography>=38.0.3 ; extra == 'testing'
- pytest ; extra == 'testing'
- pytest-cov ; extra == 'testing'
- pytest-localserver ; extra == 'testing'
- pyopenssl>=20.0.0 ; extra == 'testing'
- - cryptography>=38.0.3 ; extra == 'testing'
- pyu2f>=0.1.5 ; extra == 'testing'
- responses ; extra == 'testing'
- urllib3 ; extra == 'testing'
@@ -3369,10 +3412,10 @@ packages:
- grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc'
- grpcio-status>=1.38.0,<2.0.0 ; extra == 'grpc'
requires_python: '>=3.7'
-- pypi: https://files.pythonhosted.org/packages/2d/80/6e5c7c83cea15ed4dfc4843b9df9db0716bc551ac938f7b5dd18a72bd5e4/google_cloud_storage-3.7.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/46/0b/816a6ae3c9fd096937d2e5f9670558908811d57d59ddf69dd4b83b326fd1/google_cloud_storage-3.9.0-py3-none-any.whl
name: google-cloud-storage
- version: 3.7.0
- sha256: 469bc9540936e02f8a4bfd1619e9dca1e42dec48f95e4204d783b36476a15093
+ version: 3.9.0
+ sha256: 2dce75a9e8b3387078cbbdad44757d410ecdb916101f8ba308abf202b6968066
requires_dist:
- google-auth>=2.26.1,<3.0.0
- google-api-core>=2.27.0,<3.0.0
@@ -3390,6 +3433,28 @@ packages:
- grpc-google-iam-v1>=0.14.0,<1.0.0 ; extra == 'grpc'
- protobuf>=3.20.2,<7.0.0 ; extra == 'protobuf'
- opentelemetry-api>=1.1.0,<2.0.0 ; extra == 'tracing'
+ - google-cloud-testutils ; extra == 'testing'
+ - numpy ; extra == 'testing'
+ - psutil ; extra == 'testing'
+ - py-cpuinfo ; extra == 'testing'
+ - pytest-benchmark ; extra == 'testing'
+ - pyyaml ; extra == 'testing'
+ - mock ; extra == 'testing'
+ - pytest ; extra == 'testing'
+ - pytest-cov ; extra == 'testing'
+ - pytest-asyncio ; extra == 'testing'
+ - pytest-rerunfailures ; extra == 'testing'
+ - pytest-xdist ; extra == 'testing'
+ - google-cloud-testutils ; extra == 'testing'
+ - google-cloud-iam ; extra == 'testing'
+ - google-cloud-pubsub ; extra == 'testing'
+ - google-cloud-kms ; extra == 'testing'
+ - brotli ; extra == 'testing'
+ - coverage ; extra == 'testing'
+ - pyopenssl ; extra == 'testing'
+ - opentelemetry-sdk ; extra == 'testing'
+ - flake8 ; extra == 'testing'
+ - black ; extra == 'testing'
requires_python: '>=3.7'
- pypi: https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl
name: google-crc32c
@@ -3434,23 +3499,23 @@ packages:
- sphinx-autodoc-typehints ; extra == 'docs'
- sphinx-rtd-theme>=0.2.5 ; extra == 'docs'
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
name: grpcio
- version: 1.76.0
- sha256: 61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae
+ version: 1.78.0
+ sha256: 735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5
requires_dist:
- typing-extensions~=4.12
- - grpcio-tools>=1.76.0 ; extra == 'protobuf'
+ - grpcio-tools>=1.78.0 ; extra == 'protobuf'
requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl
name: h11
version: 0.16.0
sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/0f/d6/0da119f5fc37311b34f301e1ef60f717bf9aa289f4fed9075bf4cced0406/h3-4.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/45/a6/9cac4b5ea1dad5767a1b6dcc2eb8255a5bc5ec2c968e558484fcde328304/h3-4.4.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
name: h3
- version: 4.4.1
- sha256: adcc92cca54058746d08da20ec44b1ffb6a5c9e396fe1da0efe9e825e326ce21
+ version: 4.4.2
+ sha256: e243abf0f738d7ef5683ee52e3dd1ab0abebe9c38d05d485b5aff7cec97be3e9
requires_dist:
- numpy ; extra == 'numpy'
- pytest ; extra == 'test'
@@ -3485,14 +3550,19 @@ packages:
- h3<4 ; extra == 'test'
- pytest-benchmark ; extra == 'test'
- pyarrow>=15 ; extra == 'test'
-- pypi: https://files.pythonhosted.org/packages/d6/49/1f35189c1ca136b2f041b72402f2eb718bdcb435d9e88729fe6f6909c45d/h5netcdf-1.7.3-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/b1/8b/88f16936a8e8070a83d36239555227ecd91728f9ef222c5382cda07e0fd6/h5netcdf-1.8.1-py3-none-any.whl
name: h5netcdf
- version: 1.7.3
- sha256: b1967678127d55009edd4c7e36cb322a7b66bdade37a2e229d857f5ecf375c01
+ version: 1.8.1
+ sha256: a76ed7cfc9b8a8908ea7057c4e57e27307acff1049b7f5ed52db6c2247636879
requires_dist:
- - h5py
- packaging
+ - numpy
+ - h5py ; extra == 'h5py'
+ - pyfive>=1.0.0 ; extra == 'pyfive'
+ - h5pyd ; extra == 'h5pyd'
+ - h5py ; extra == 'test'
- netcdf4 ; extra == 'test'
+ - pyfive>=1.0.0 ; extra == 'test'
- pytest ; extra == 'test'
requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
@@ -3538,12 +3608,12 @@ packages:
- socksio==1.* ; extra == 'socks'
- trio>=0.22.0,<1.0 ; extra == 'trio'
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/8c/a2/0d269db0f6163be503775dc8b6a6fa15820cc9fdc866f6ba608d86b721f2/httplib2-0.31.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl
name: httplib2
- version: 0.31.0
- sha256: b9cd78abea9b4e43a7714c6e0f8b6b8561a6fc1e95d5dbd367f5bf0ef35f5d24
+ version: 0.31.2
+ sha256: dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349
requires_dist:
- - pyparsing>=3.0.4,<4
+ - pyparsing>=3.1,<4
requires_python: '>=3.6'
- pypi: https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
name: httptools
@@ -3568,109 +3638,40 @@ packages:
- socksio==1.* ; extra == 'socks'
- zstandard>=0.18.0 ; extra == 'zstd'
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl
name: huggingface-hub
- version: 0.36.0
- sha256: 7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d
+ version: 0.36.2
+ sha256: 48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270
requires_dist:
- filelock
- fsspec>=2023.5.0
+ - hf-xet>=1.1.3,<2.0.0 ; platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'
- packaging>=20.9
- pyyaml>=5.1
- requests
- tqdm>=4.42.1
- typing-extensions>=3.7.4.3
- - hf-xet>=1.1.3,<2.0.0 ; platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'
- - inquirerpy==0.3.4 ; extra == 'all'
- - aiohttp ; extra == 'all'
- - authlib>=1.3.2 ; extra == 'all'
- - fastapi ; extra == 'all'
- - httpx ; extra == 'all'
- - itsdangerous ; extra == 'all'
- - jedi ; extra == 'all'
- - jinja2 ; extra == 'all'
- - pytest>=8.1.1,<8.2.2 ; extra == 'all'
- - pytest-cov ; extra == 'all'
- - pytest-env ; extra == 'all'
- - pytest-xdist ; extra == 'all'
- - pytest-vcr ; extra == 'all'
- - pytest-asyncio ; extra == 'all'
- - pytest-rerunfailures<16.0 ; extra == 'all'
- - pytest-mock ; extra == 'all'
- - urllib3<2.0 ; extra == 'all'
- - soundfile ; extra == 'all'
- - pillow ; extra == 'all'
- - gradio>=4.0.0 ; extra == 'all'
- - numpy ; extra == 'all'
- - ruff>=0.9.0 ; extra == 'all'
- - libcst>=1.4.0 ; extra == 'all'
- - ty ; extra == 'all'
- - typing-extensions>=4.8.0 ; extra == 'all'
- - types-pyyaml ; extra == 'all'
- - types-requests ; extra == 'all'
- - types-simplejson ; extra == 'all'
- - types-toml ; extra == 'all'
- - types-tqdm ; extra == 'all'
- - types-urllib3 ; extra == 'all'
- - mypy>=1.14.1,<1.15.0 ; python_full_version == '3.8.*' and extra == 'all'
- - mypy==1.15.0 ; python_full_version >= '3.9' and extra == 'all'
- inquirerpy==0.3.4 ; extra == 'cli'
- - inquirerpy==0.3.4 ; extra == 'dev'
- - aiohttp ; extra == 'dev'
- - authlib>=1.3.2 ; extra == 'dev'
- - fastapi ; extra == 'dev'
- - httpx ; extra == 'dev'
- - itsdangerous ; extra == 'dev'
- - jedi ; extra == 'dev'
- - jinja2 ; extra == 'dev'
- - pytest>=8.1.1,<8.2.2 ; extra == 'dev'
- - pytest-cov ; extra == 'dev'
- - pytest-env ; extra == 'dev'
- - pytest-xdist ; extra == 'dev'
- - pytest-vcr ; extra == 'dev'
- - pytest-asyncio ; extra == 'dev'
- - pytest-rerunfailures<16.0 ; extra == 'dev'
- - pytest-mock ; extra == 'dev'
- - urllib3<2.0 ; extra == 'dev'
- - soundfile ; extra == 'dev'
- - pillow ; extra == 'dev'
- - gradio>=4.0.0 ; extra == 'dev'
- - numpy ; extra == 'dev'
- - ruff>=0.9.0 ; extra == 'dev'
- - libcst>=1.4.0 ; extra == 'dev'
- - ty ; extra == 'dev'
- - typing-extensions>=4.8.0 ; extra == 'dev'
- - types-pyyaml ; extra == 'dev'
- - types-requests ; extra == 'dev'
- - types-simplejson ; extra == 'dev'
- - types-toml ; extra == 'dev'
- - types-tqdm ; extra == 'dev'
- - types-urllib3 ; extra == 'dev'
- - mypy>=1.14.1,<1.15.0 ; python_full_version == '3.8.*' and extra == 'dev'
- - mypy==1.15.0 ; python_full_version >= '3.9' and extra == 'dev'
- - toml ; extra == 'fastai'
- - fastai>=2.4 ; extra == 'fastai'
- - fastcore>=1.3.27 ; extra == 'fastai'
- - hf-transfer>=0.1.4 ; extra == 'hf-transfer'
- - hf-xet>=1.1.2,<2.0.0 ; extra == 'hf-xet'
- aiohttp ; extra == 'inference'
- - mcp>=1.8.0 ; extra == 'mcp'
- - typer ; extra == 'mcp'
- - aiohttp ; extra == 'mcp'
- authlib>=1.3.2 ; extra == 'oauth'
- fastapi ; extra == 'oauth'
- httpx ; extra == 'oauth'
- itsdangerous ; extra == 'oauth'
- - ruff>=0.9.0 ; extra == 'quality'
- - libcst>=1.4.0 ; extra == 'quality'
- - ty ; extra == 'quality'
- - mypy>=1.14.1,<1.15.0 ; python_full_version == '3.8.*' and extra == 'quality'
- - mypy==1.15.0 ; python_full_version >= '3.9' and extra == 'quality'
+ - torch ; extra == 'torch'
+ - safetensors[torch] ; extra == 'torch'
+ - hf-transfer>=0.1.4 ; extra == 'hf-transfer'
+ - toml ; extra == 'fastai'
+ - fastai>=2.4 ; extra == 'fastai'
+ - fastcore>=1.3.27 ; extra == 'fastai'
- tensorflow ; extra == 'tensorflow'
- pydot ; extra == 'tensorflow'
- graphviz ; extra == 'tensorflow'
- tensorflow ; extra == 'tensorflow-testing'
- keras<3.0 ; extra == 'tensorflow-testing'
+ - hf-xet>=1.1.2,<2.0.0 ; extra == 'hf-xet'
+ - mcp>=1.8.0 ; extra == 'mcp'
+ - typer ; extra == 'mcp'
+ - aiohttp ; extra == 'mcp'
- inquirerpy==0.3.4 ; extra == 'testing'
- aiohttp ; extra == 'testing'
- authlib>=1.3.2 ; extra == 'testing'
@@ -3692,8 +3693,7 @@ packages:
- pillow ; extra == 'testing'
- gradio>=4.0.0 ; extra == 'testing'
- numpy ; extra == 'testing'
- - torch ; extra == 'torch'
- - safetensors[torch] ; extra == 'torch'
+ - fastapi ; extra == 'testing'
- typing-extensions>=4.8.0 ; extra == 'typing'
- types-pyyaml ; extra == 'typing'
- types-requests ; extra == 'typing'
@@ -3701,6 +3701,79 @@ packages:
- types-toml ; extra == 'typing'
- types-tqdm ; extra == 'typing'
- types-urllib3 ; extra == 'typing'
+ - ruff>=0.9.0 ; extra == 'quality'
+ - mypy>=1.14.1,<1.15.0 ; python_full_version == '3.8.*' and extra == 'quality'
+ - mypy==1.15.0 ; python_full_version >= '3.9' and extra == 'quality'
+ - libcst>=1.4.0 ; extra == 'quality'
+ - ty ; extra == 'quality'
+ - inquirerpy==0.3.4 ; extra == 'all'
+ - aiohttp ; extra == 'all'
+ - authlib>=1.3.2 ; extra == 'all'
+ - fastapi ; extra == 'all'
+ - httpx ; extra == 'all'
+ - itsdangerous ; extra == 'all'
+ - jedi ; extra == 'all'
+ - jinja2 ; extra == 'all'
+ - pytest>=8.1.1,<8.2.2 ; extra == 'all'
+ - pytest-cov ; extra == 'all'
+ - pytest-env ; extra == 'all'
+ - pytest-xdist ; extra == 'all'
+ - pytest-vcr ; extra == 'all'
+ - pytest-asyncio ; extra == 'all'
+ - pytest-rerunfailures<16.0 ; extra == 'all'
+ - pytest-mock ; extra == 'all'
+ - urllib3<2.0 ; extra == 'all'
+ - soundfile ; extra == 'all'
+ - pillow ; extra == 'all'
+ - gradio>=4.0.0 ; extra == 'all'
+ - numpy ; extra == 'all'
+ - fastapi ; extra == 'all'
+ - ruff>=0.9.0 ; extra == 'all'
+ - mypy>=1.14.1,<1.15.0 ; python_full_version == '3.8.*' and extra == 'all'
+ - mypy==1.15.0 ; python_full_version >= '3.9' and extra == 'all'
+ - libcst>=1.4.0 ; extra == 'all'
+ - ty ; extra == 'all'
+ - typing-extensions>=4.8.0 ; extra == 'all'
+ - types-pyyaml ; extra == 'all'
+ - types-requests ; extra == 'all'
+ - types-simplejson ; extra == 'all'
+ - types-toml ; extra == 'all'
+ - types-tqdm ; extra == 'all'
+ - types-urllib3 ; extra == 'all'
+ - inquirerpy==0.3.4 ; extra == 'dev'
+ - aiohttp ; extra == 'dev'
+ - authlib>=1.3.2 ; extra == 'dev'
+ - fastapi ; extra == 'dev'
+ - httpx ; extra == 'dev'
+ - itsdangerous ; extra == 'dev'
+ - jedi ; extra == 'dev'
+ - jinja2 ; extra == 'dev'
+ - pytest>=8.1.1,<8.2.2 ; extra == 'dev'
+ - pytest-cov ; extra == 'dev'
+ - pytest-env ; extra == 'dev'
+ - pytest-xdist ; extra == 'dev'
+ - pytest-vcr ; extra == 'dev'
+ - pytest-asyncio ; extra == 'dev'
+ - pytest-rerunfailures<16.0 ; extra == 'dev'
+ - pytest-mock ; extra == 'dev'
+ - urllib3<2.0 ; extra == 'dev'
+ - soundfile ; extra == 'dev'
+ - pillow ; extra == 'dev'
+ - gradio>=4.0.0 ; extra == 'dev'
+ - numpy ; extra == 'dev'
+ - fastapi ; extra == 'dev'
+ - ruff>=0.9.0 ; extra == 'dev'
+ - mypy>=1.14.1,<1.15.0 ; python_full_version == '3.8.*' and extra == 'dev'
+ - mypy==1.15.0 ; python_full_version >= '3.9' and extra == 'dev'
+ - libcst>=1.4.0 ; extra == 'dev'
+ - ty ; extra == 'dev'
+ - typing-extensions>=4.8.0 ; extra == 'dev'
+ - types-pyyaml ; extra == 'dev'
+ - types-requests ; extra == 'dev'
+ - types-simplejson ; extra == 'dev'
+ - types-toml ; extra == 'dev'
+ - types-tqdm ; extra == 'dev'
+ - types-urllib3 ; extra == 'dev'
requires_python: '>=3.8.0'
- pypi: https://files.pythonhosted.org/packages/b6/cd/5b3334d39276067f54618ce0d0b48ed69d91352fbf137468c7095170d0e5/hyperopt-0.2.7-py2.py3-none-any.whl
name: hyperopt
@@ -3723,30 +3796,12 @@ packages:
- pre-commit ; extra == 'dev'
- nose ; extra == 'dev'
- pytest ; extra == 'dev'
-- pypi: https://files.pythonhosted.org/packages/8c/d7/db466e07a21553441adbf915f0913a3f8fecece364cacb2392f11be267be/icechunk-1.1.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/1c/71/043ec2eb964d871f916ee949f99b71dbbd31ffe85c5a280edaaa6f877789/icechunk-1.1.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
name: icechunk
- version: 1.1.15
- sha256: c9e0cc3c8623a48861470553dbb8b0f1e86600989f597ce41ecf47568d8d099d
+ version: 1.1.18
+ sha256: 02a11c9c79dda045777ecde0f948c738f918396e74ad5e1d6de069b0db0f14d1
requires_dist:
- zarr>=3,!=3.0.3
- - boto3 ; extra == 'test'
- - coverage ; extra == 'test'
- - mypy ; extra == 'test'
- - object-store-python ; extra == 'test'
- - pytest ; extra == 'test'
- - pytest-cov ; extra == 'test'
- - pytest-asyncio ; extra == 'test'
- - pytest-xdist ; extra == 'test'
- - ruff ; extra == 'test'
- - dask>=2024.11.0 ; extra == 'test'
- - distributed>=2024.11.0 ; extra == 'test'
- - xarray>=2025.1.2 ; extra == 'test'
- - hypothesis ; extra == 'test'
- - pandas-stubs ; extra == 'test'
- - boto3-stubs[s3] ; extra == 'test'
- - termcolor ; extra == 'test'
- - pooch ; extra == 'test'
- - netcdf4 ; extra == 'test'
- pytest-benchmark[histogram] ; extra == 'benchmark'
- pytest-xdist ; extra == 'benchmark'
- s3fs ; extra == 'benchmark'
@@ -3787,6 +3842,24 @@ packages:
- sphinx-design ; extra == 'docs'
- sphinx-togglebutton ; extra == 'docs'
- sphinx-autodoc-typehints ; extra == 'docs'
+ - boto3 ; extra == 'test'
+ - coverage ; extra == 'test'
+ - mypy ; extra == 'test'
+ - object-store-python ; extra == 'test'
+ - pytest ; extra == 'test'
+ - pytest-cov ; extra == 'test'
+ - pytest-asyncio ; extra == 'test'
+ - pytest-xdist ; extra == 'test'
+ - ruff ; extra == 'test'
+ - dask>=2024.11.0 ; extra == 'test'
+ - distributed>=2024.11.0 ; extra == 'test'
+ - xarray>=2025.1.2 ; extra == 'test'
+ - hypothesis ; extra == 'test'
+ - pandas-stubs ; extra == 'test'
+ - boto3-stubs[s3] ; extra == 'test'
+ - termcolor ; extra == 'test'
+ - pooch ; extra == 'test'
+ - netcdf4 ; extra == 'test'
requires_python: '>=3.11'
- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda
sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e
@@ -3810,10 +3883,10 @@ packages:
- pytest>=8.3.2 ; extra == 'all'
- flake8>=7.1.1 ; extra == 'all'
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/59/81/d967317829a265c3a25c69791f7d2c8ce529d4bbcb1d8312a3cd1c6d2886/imagecodecs-2026.1.1-cp311-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/02/e1/8a299b91a4abee7c299c0c6625f9c0985c623fd4b6b41b5a5fe92508bb18/imagecodecs-2026.1.14-cp311-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
name: imagecodecs
- version: 2026.1.1
- sha256: ad867cd5910af53ebfb19f745eaee7288e0a60674302dbd0f01707a5ba522b24
+ version: 2026.1.14
+ sha256: 8a78451926905459f42e827c207d80e1601abf68eaf0e38fd65ddd3c346d1f47
requires_dist:
- numpy
- matplotlib ; extra == 'all'
@@ -3900,6 +3973,27 @@ packages:
- sphinx<6 ; extra == 'full'
- tifffile ; extra == 'full'
requires_python: '>=3.9'
+- pypi: https://files.pythonhosted.org/packages/e8/10/530b51352d3dcbce5f242e9d9c07a4ece5a983af5d4cceb8c6fad4e8e087/imodels-2.0.4-py3-none-any.whl
+ name: imodels
+ version: 2.0.4
+ sha256: ec9038de1017ff6596038ac994ecec1625fdec3a6ba96b88304d1e94e4247767
+ requires_dist:
+ - matplotlib
+ - mlxtend
+ - numpy
+ - pandas
+ - requests
+ - scipy
+ - scikit-learn
+ - tqdm
+ - cvxpy ; extra == 'optional'
+ - gosdt-deprecated ; extra == 'optional'
+ - irf ; extra == 'optional'
+ - statsmodels ; extra == 'optional'
+ - torch ; extra == 'optional'
+ - interpret ; extra == 'optional'
+ - glmnet ; extra == 'optional'
+ requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl
name: importlib-metadata
version: 8.7.1
@@ -3931,6 +4025,45 @@ packages:
version: 2.3.0
sha256: f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
requires_python: '>=3.10'
+- pypi: https://files.pythonhosted.org/packages/2e/6e/3fa71d85753438dc926ba582194a6fc4139d213b4a7ed72741962020110d/interpret_core-0.7.5-py3-none-any.whl
+ name: interpret-core
+ version: 0.7.5
+ sha256: bb687a04900c3f7e2a786dd25d845b09d500eebe32e9826c9696e289b02bfd8c
+ requires_dist:
+ - numpy>=1.25
+ - pandas>=0.24
+ - scikit-learn>=1.6.0
+ - joblib>=0.11
+ - psutil>=5.6.2 ; extra == 'debug'
+ - ipykernel>=4.10.0 ; extra == 'notebook'
+ - ipython>=5.5.0 ; extra == 'notebook'
+ - plotly>=3.8.1 ; extra == 'plotly'
+ - xlsxwriter>=3.0.1 ; extra == 'excel'
+ - dotsi>=0.0.3 ; extra == 'excel'
+ - seaborn>=0.13.2 ; extra == 'excel'
+ - matplotlib>=3.9.1 ; extra == 'excel'
+ - lime>=0.1.1.33 ; extra == 'lime'
+ - salib>=1.3.3 ; extra == 'sensitivity'
+ - shap>=0.28.5 ; extra == 'shap'
+ - dill>=0.2.5 ; extra == 'shap'
+ - skope-rules>=1.0.1 ; extra == 'skoperules'
+ - treeinterpreter>=0.2.2 ; extra == 'treeinterpreter'
+ - aplr>=10.6.1 ; extra == 'aplr'
+ - dash>=2.0.0,<3.0.0 ; extra == 'dash'
+ - dash-cytoscape>=0.1.1 ; extra == 'dash'
+ - gevent>=1.3.6 ; extra == 'dash'
+ - requests>=2.19.0 ; extra == 'dash'
+ - scipy>=0.18.1 ; extra == 'testing'
+ - scikit-learn>=1.0.0 ; extra == 'testing'
+ - pytest>=4.3.0 ; extra == 'testing'
+ - pytest-runner>=4.4 ; extra == 'testing'
+ - pytest-xdist>=1.29 ; extra == 'testing'
+ - nbconvert>=5.4.1 ; extra == 'testing'
+ - selenium>=3.141.0 ; extra == 'testing'
+ - pytest-cov>=2.6.1 ; extra == 'testing'
+ - ruff>=0.1.2 ; extra == 'testing'
+ - jupyter>=1.0.0 ; extra == 'testing'
+ - ipywidgets>=7.4.2 ; extra == 'testing'
- pypi: https://files.pythonhosted.org/packages/4c/0f/b66d63d4a5426c09005d3713b056e634e00e69788fdc88d1ffe40e5b7654/ipycytoscape-1.3.3-py2.py3-none-any.whl
name: ipycytoscape
version: 1.3.3
@@ -3978,23 +4111,23 @@ packages:
sha256: 4555c24b30b819c91dc0ae5e6f7e4cf8f90e5cca531a9209a1fe4deee288d5c5
requires_dist:
- ipywidgets
-- pypi: https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl
name: ipykernel
- version: 7.1.0
- sha256: 763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c
+ version: 7.2.0
+ sha256: 3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661
requires_dist:
- appnope>=0.1.2 ; sys_platform == 'darwin'
- comm>=0.1.1
- debugpy>=1.6.5
- ipython>=7.23.1
- - jupyter-client>=8.0.0
- - jupyter-core>=4.12,!=5.0.*
+ - jupyter-client>=8.8.0
+ - jupyter-core>=5.1,!=6.0.*
- matplotlib-inline>=0.1
- nest-asyncio>=1.4
- packaging>=22
- psutil>=5.7
- pyzmq>=25
- - tornado>=6.2
+ - tornado>=6.4.1
- traitlets>=5.4.0
- coverage[toml] ; extra == 'cov'
- matplotlib ; extra == 'cov'
@@ -4016,7 +4149,7 @@ packages:
- pytest-asyncio>=0.23.5 ; extra == 'test'
- pytest-cov ; extra == 'test'
- pytest-timeout ; extra == 'test'
- - pytest>=7.0,<9 ; extra == 'test'
+ - pytest>=7.0,<10 ; extra == 'test'
requires_python: '>=3.10'
- pypi: https://files.pythonhosted.org/packages/49/69/e9858f2c0b99bf9f036348d1c84b8026f438bb6875effe6a9bcd9883dada/ipyleaflet-0.20.0-py3-none-any.whl
name: ipyleaflet
@@ -4029,10 +4162,10 @@ packages:
- traittypes>=0.2.1,<3
- xyzservices>=2021.8.1
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl
name: ipython
- version: 9.9.0
- sha256: b457fe9165df2b84e8ec909a97abcf2ed88f565970efba16b1f7229c283d252b
+ version: 9.10.0
+ sha256: c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d
requires_dist:
- colorama>=0.4.4 ; sys_platform == 'win32'
- decorator>=4.3.2
@@ -4161,11 +4294,11 @@ packages:
- pkg:pypi/jinja2?source=compressed-mapping
size: 120685
timestamp: 1764517220861
-- pypi: https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl
name: jmespath
- version: 1.0.1
- sha256: 02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980
- requires_python: '>=3.7'
+ version: 1.1.0
+ sha256: a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64
+ requires_python: '>=3.9'
- conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda
sha256: 301539229d7be6420c084490b8145583291123f0ce6b92f56be5948a2c83a379
md5: 615de2a4d97af50c350e5cf160149e77
@@ -5573,6 +5706,21 @@ packages:
- pandas>=0.24.0 ; extra == 'pandas'
- scikit-learn>=0.24.2 ; extra == 'scikit-learn'
requires_python: '>=3.7'
+- pypi: https://files.pythonhosted.org/packages/de/73/3d757cb3fc16f0f9794dd289bcd0c4a031d9cf54d8137d6b984b2d02edf3/lightning_utilities-0.15.2-py3-none-any.whl
+ name: lightning-utilities
+ version: 0.15.2
+ sha256: ad3ab1703775044bbf880dbf7ddaaac899396c96315f3aa1779cec9d618a9841
+ requires_dist:
+ - importlib-metadata>=4.0.0 ; python_full_version < '3.8'
+ - packaging>=17.1
+ - setuptools
+ - typing-extensions
+ - requests>=2.0.0 ; extra == 'docs'
+ - mypy>=1.0.0 ; extra == 'typing'
+ - types-setuptools ; extra == 'typing'
+ - jsonargparse[signatures]>=4.38.0 ; extra == 'cli'
+ - tomlkit ; extra == 'cli'
+ requires_python: '>=3.9'
- conda: https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-21.1.8-h4922eb0_0.conda
sha256: a5a7ad16eecbe35cac63e529ea9c261bef4ccdd68cb1db247409f04529423989
md5: f8640b709b37dc7758ddce45ea18d000
@@ -5875,6 +6023,22 @@ packages:
purls: []
size: 125177250
timestamp: 1761668323993
+- pypi: https://files.pythonhosted.org/packages/4c/43/2fc7f76c8891aef148901f1ba3dee65c1cbac00a85ae5ee0dabc2b861256/mlxtend-0.23.4-py3-none-any.whl
+ name: mlxtend
+ version: 0.23.4
+ sha256: 8675456e2b71841116e5317f6d7aa568848ea2546865eb5eca7192e9b7f395f4
+ requires_dist:
+ - scipy>=1.2.1
+ - numpy>=1.16.2
+ - pandas>=0.24.2
+ - scikit-learn>=1.3.1
+ - matplotlib>=3.0.0
+ - joblib>=0.13.2
+ - pytest ; extra == 'testing'
+ - mkdocs ; extra == 'docs'
+ - python-markdown-math ; extra == 'docs'
+ - mkdocs-bootswatch ; extra == 'docs'
+ - nbconvert ; extra == 'docs'
- conda: https://conda.anaconda.org/conda-forge/linux-64/mpc-1.3.1-h24ddda3_1.conda
sha256: 1bf794ddf2c8b3a3e14ae182577c624fa92dea975537accff4bc7e5fea085212
md5: aa14b9a5196a6d8dd364164b7ce56acf
@@ -5916,10 +6080,10 @@ packages:
version: 1.1.2
sha256: fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
name: multidict
- version: 6.7.0
- sha256: 9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32
+ version: 6.7.1
+ sha256: 9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429
requires_dist:
- typing-extensions>=4.1.0 ; python_full_version < '3.11'
requires_python: '>=3.9'
@@ -5941,12 +6105,12 @@ packages:
version: 1.0.15
sha256: b3ba6d05de2613535b5a9227d4ad8ef40a540465f64660d4a8800634ae10e04f
requires_python: '>=3.6,<3.15'
-- pypi: https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl
name: narwhals
- version: 2.15.0
- sha256: cbfe21ca19d260d9fd67f995ec75c44592d1f106933b03ddd375df7ac841f9d6
+ version: 2.16.0
+ sha256: 846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d
requires_dist:
- - cudf>=24.10.0 ; extra == 'cudf'
+ - cudf-cu12>=24.10.0 ; extra == 'cudf'
- dask[dataframe]>=2024.8 ; extra == 'dask'
- duckdb>=1.1 ; extra == 'duckdb'
- ibis-framework>=6.0.0 ; extra == 'ibis'
@@ -5959,6 +6123,8 @@ packages:
- pyarrow>=13.0.0 ; extra == 'pyarrow'
- pyspark>=3.5.0 ; extra == 'pyspark'
- pyspark[connect]>=3.5.0 ; extra == 'pyspark-connect'
+ - duckdb>=1.1 ; extra == 'sql'
+ - sqlparse ; extra == 'sql'
- sqlframe>=3.22.0,!=3.39.3 ; extra == 'sqlframe'
requires_python: '>=3.9'
- conda: https://conda.anaconda.org/conda-forge/linux-64/nccl-2.28.9.1-h4d09622_1.conda
@@ -6151,10 +6317,10 @@ packages:
- pkg:pypi/nvidia-ml-py?source=hash-mapping
size: 48971
timestamp: 1765209768013
-- pypi: https://files.pythonhosted.org/packages/23/2d/609d0392d992259c6dc39881688a7fc13b1397a668bc360fbd68d1396f85/nvidia_nccl_cu12-2.29.2-py3-none-manylinux_2_18_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/31/5a/cac7d231f322b66caa16fd4b136ebc8e4b18b2805811c2d58dc47210cdea/nvidia_nccl_cu12-2.29.3-py3-none-manylinux_2_18_x86_64.whl
name: nvidia-nccl-cu12
- version: 2.29.2
- sha256: 3a9a0bf4142126e0d0ed99ec202579bef8d007601f9fab75af60b10324666b12
+ version: 2.29.3
+ sha256: 35ad42e7d5d722a83c36a3a478e281c20a5646383deaf1b9ed1a9ab7d61bed53
requires_python: '>=3'
- conda: https://conda.anaconda.org/conda-forge/linux-64/nvtx-0.2.14-py313h07c4f96_0.conda
sha256: 9341cb332428242ab938c5fc202008c12430ec43b8b83511d327f14bf8fd6d96
@@ -6235,10 +6401,10 @@ packages:
- ipyleaflet ; extra == 'test-all'
- matplotlib ; extra == 'test-all'
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/92/58/5148ae95713c84255267b26eddfe1ac926244aca4327bdeb4cf2d6d576bf/odc_loader-0.6.2-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/8e/8f/22b0a79e458392e377a9784cc93e1bd6aaa3f908d52969a81e0c56e6b59d/odc_loader-0.6.4-py3-none-any.whl
name: odc-loader
- version: 0.6.2
- sha256: fb02ac4d76316b23449f04cfe777901826ef907140c61c6f36de2af795b65011
+ version: 0.6.4
+ sha256: 3f936841c253535fdb35fb803579e40e5f438d2fa893596004287143817cfee0
requires_dist:
- odc-geo>=0.4.7
- rasterio>=1.0.0,!=1.3.0,!=1.3.1
@@ -6249,14 +6415,14 @@ packages:
- botocore ; extra == 'botocore'
- zarr>=2.18.3,<4 ; extra == 'zarr'
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/e2/c7/b8f2b3e53f26f8f463002f3e8023189653b627b22ba6c00ef86eaba50b73/odc_stac-0.5.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/01/8a/fd93f13a9b4b688b5ac8c8ec14ed6401db99166b7d55c41c9c96250398d9/odc_stac-0.5.2-py3-none-any.whl
name: odc-stac
- version: 0.5.0
- sha256: 02c3161242a8032d8d769c477d1668d563ef5c6e44617f3fdb273dfca1c20136
+ version: 0.5.2
+ sha256: acb15b7dd26771fdcdf4f93b1e070e14748fad6d18c572e6710f8ca5d6c78073
requires_dist:
- affine
- odc-geo>=0.4.7
- - odc-loader>=0.6.0
+ - odc-loader>=0.6.4
- rasterio>=1.0.0,!=1.3.0,!=1.3.1
- dask[array]
- numpy>=1.20.0
@@ -6469,18 +6635,18 @@ packages:
- pkg:pypi/pandas?source=hash-mapping
size: 14912799
timestamp: 1764615091147
-- pypi: https://files.pythonhosted.org/packages/64/20/69f2a39792a653fd64d916cd563ed79ec6e5dcfa6408c4674021d810afcf/pandas_stubs-2.3.3.251219-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/d1/c6/df1fe324248424f77b89371116dab5243db7f052c32cc9fe7442ad9c5f75/pandas_stubs-2.3.3.260113-py3-none-any.whl
name: pandas-stubs
- version: 2.3.3.251219
- sha256: ccc6337febb51d6d8a08e4c96b479478a0da0ef704b5e08bd212423fe1cb549c
+ version: 2.3.3.260113
+ sha256: ec070b5c576e1badf12544ae50385872f0631fc35d99d00dc598c2954ec564d3
requires_dist:
- - numpy>=1.23.5,<=2.3.5
+ - numpy>=1.23.5
- types-pytz>=2022.1.1
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/11/da/9d476e9aadfa854719f3cb917e3f7a170a657a182d8d1d6e546594a4872b/param-2.3.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/11/b6/f8c7e1f5f716e16070cf35f90c24f95f397376bb810e65000b6bc55950cc/param-2.3.2-py3-none-any.whl
name: param
- version: 2.3.1
- sha256: 886b19031438719bbecfd15044dcdd9ed3cb9edb199191294f75600c7081d163
+ version: 2.3.2
+ sha256: 147717b21cf2d8add08edb135f678c5fda08a701dc69e0897d75812e4c2af365
requires_dist:
- aiohttp ; extra == 'all'
- cloudpickle ; extra == 'all'
@@ -6612,10 +6778,10 @@ packages:
- trove-classifiers>=2024.10.12 ; extra == 'tests'
- defusedxml ; extra == 'xmp'
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl
name: pip
- version: '25.3'
- sha256: 9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
+ version: 26.0.1
+ sha256: bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b
requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl
name: platformdirs
@@ -6633,10 +6799,10 @@ packages:
- pytest>=8.4.2 ; extra == 'test'
- mypy>=1.18.2 ; extra == 'type'
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/e9/8e/24e0bb90b2d75af84820693260c5534e9ed351afdda67ed6f393a141a0e2/plotly-6.5.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/8a/67/f95b5460f127840310d2187f916cf0023b5875c0717fdf893f71e1325e87/plotly-6.5.2-py3-none-any.whl
name: plotly
- version: 6.5.1
- sha256: 5adad4f58c360612b6c5ce11a308cdbc4fd38ceb1d40594a614f0062e227abe1
+ version: 6.5.2
+ sha256: 91757653bd9c550eeea2fa2404dba6b85d1e366d54804c340b2c874e5a7eb4a4
requires_dist:
- narwhals>=1.15.1
- packaging
@@ -6693,10 +6859,10 @@ packages:
- rich>=10.0
- typing-extensions>=4.9.0
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl
name: pooch
- version: 1.8.2
- sha256: 3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47
+ version: 1.9.0
+ sha256: f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b
requires_dist:
- platformdirs>=2.5.0
- packaging>=20.0
@@ -6704,7 +6870,57 @@ packages:
- tqdm>=4.41.0,<5.0.0 ; extra == 'progress'
- paramiko>=2.7.0 ; extra == 'sftp'
- xxhash>=1.4.3 ; extra == 'xxhash'
- requires_python: '>=3.7'
+ - pytest-httpserver ; extra == 'test'
+ - pytest-localftpserver ; extra == 'test'
+ requires_python: '>=3.9'
+- pypi: https://files.pythonhosted.org/packages/a7/4b/e4759803793843ce2258ddef3b3a744bdf0318c77c3ac10560683a2eee60/posthog-6.9.3-py3-none-any.whl
+ name: posthog
+ version: 6.9.3
+ sha256: c71e9cb7ac4ef13eb604f04c3161edd10b1d08a32499edd54437ba5eab591c58
+ requires_dist:
+ - requests>=2.7,<3.0
+ - six>=1.5
+ - python-dateutil>=2.2
+ - backoff>=1.10.0
+ - distro>=1.5.0
+ - typing-extensions>=4.2.0
+ - langchain>=0.2.0 ; extra == 'langchain'
+ - django-stubs ; extra == 'dev'
+ - lxml ; extra == 'dev'
+ - mypy ; extra == 'dev'
+ - mypy-baseline ; extra == 'dev'
+ - types-mock ; extra == 'dev'
+ - types-python-dateutil ; extra == 'dev'
+ - types-requests ; extra == 'dev'
+ - types-setuptools ; extra == 'dev'
+ - types-six ; extra == 'dev'
+ - pre-commit ; extra == 'dev'
+ - pydantic ; extra == 'dev'
+ - ruff ; extra == 'dev'
+ - setuptools ; extra == 'dev'
+ - packaging ; extra == 'dev'
+ - wheel ; extra == 'dev'
+ - twine ; extra == 'dev'
+ - tomli ; extra == 'dev'
+ - tomli-w ; extra == 'dev'
+ - mock>=2.0.0 ; extra == 'test'
+ - freezegun==1.5.1 ; extra == 'test'
+ - coverage ; extra == 'test'
+ - pytest ; extra == 'test'
+ - pytest-timeout ; extra == 'test'
+ - pytest-asyncio ; extra == 'test'
+ - django ; extra == 'test'
+ - openai ; extra == 'test'
+ - anthropic ; extra == 'test'
+ - langgraph>=0.4.8 ; extra == 'test'
+ - langchain-core>=0.3.65 ; extra == 'test'
+ - langchain-community>=0.3.25 ; extra == 'test'
+ - langchain-openai>=0.3.22 ; extra == 'test'
+ - langchain-anthropic>=0.3.15 ; extra == 'test'
+ - google-genai ; extra == 'test'
+ - pydantic ; extra == 'test'
+ - parameterized>=0.8.1 ; extra == 'test'
+ requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/17/73/f388398f8d789f69b510272d144a9186d658423f6d3ecc484c0fe392acec/preshed-3.0.12-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl
name: preshed
version: 3.0.12
@@ -6713,12 +6929,14 @@ packages:
- cymem>=2.0.2,<2.1.0
- murmurhash>=0.28.0,<1.1.0
requires_python: '>=3.6,<3.15'
-- pypi: https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl
name: prometheus-client
- version: 0.23.1
- sha256: dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99
+ version: 0.24.1
+ sha256: 150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055
requires_dist:
- twisted ; extra == 'twisted'
+ - aiohttp ; extra == 'aiohttp'
+ - django ; extra == 'django'
requires_python: '>=3.9'
- conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda
sha256: 013669433eb447548f21c3c6b16b2ed64356f726b5f77c1b39d5ba17a8a4b8bc
@@ -6747,18 +6965,18 @@ packages:
version: 0.4.1
sha256: d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl
name: proto-plus
- version: 1.27.0
- sha256: 1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82
+ version: 1.27.1
+ sha256: e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc
requires_dist:
- protobuf>=3.19.0,<7.0.0
- google-api-core>=1.31.5 ; extra == 'testing'
requires_python: '>=3.7'
-- pypi: https://files.pythonhosted.org/packages/38/8c/6522b8e543ece46f645911c3cebe361d8460134c0fee02ddcf70ebf32999/protobuf-6.33.3-cp39-abi3-manylinux2014_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl
name: protobuf
- version: 6.33.3
- sha256: 6fa9b5f4baa12257542273e5e6f3c3d3867b30bc2770c14ad9ac8315264bf986
+ version: 6.33.5
+ sha256: cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0
requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl
name: psutil
@@ -6868,10 +7086,10 @@ packages:
- pkg:pypi/pyarrow?source=hash-mapping
size: 5216780
timestamp: 1746000628209
-- pypi: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl
name: pyasn1
- version: 0.6.1
- sha256: 0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629
+ version: 0.6.2
+ sha256: 1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf
requires_python: '>=3.8'
- pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl
name: pyasn1-modules
@@ -6918,11 +7136,11 @@ packages:
- pkg:pypi/pybind11-global?source=hash-mapping
size: 228871
timestamp: 1755953338243
-- pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl
name: pycparser
- version: '2.23'
- sha256: e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
- requires_python: '>=3.8'
+ version: '3.0'
+ sha256: b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
+ requires_python: '>=3.10'
- pypi: https://files.pythonhosted.org/packages/8c/b2/23f4032cd1c9744aa8e9ecda43cd4d755fcb209f7f40fae035248f31a679/pyct-0.6.0-py3-none-any.whl
name: pyct
version: 0.6.0
@@ -6952,6 +7170,22 @@ packages:
requires_dist:
- typing-extensions>=4.14.1
requires_python: '>=3.9'
+- pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl
+ name: pydantic-settings
+ version: 2.12.0
+ sha256: fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809
+ requires_dist:
+ - pydantic>=2.7.0
+ - python-dotenv>=0.21.0
+ - typing-inspection>=0.4.0
+ - boto3-stubs[secretsmanager] ; extra == 'aws-secrets-manager'
+ - boto3>=1.35.0 ; extra == 'aws-secrets-manager'
+ - azure-identity>=1.16.0 ; extra == 'azure-key-vault'
+ - azure-keyvault-secrets>=4.8.0 ; extra == 'azure-key-vault'
+ - google-cloud-secret-manager>=2.23.1 ; extra == 'gcp-secret-manager'
+ - tomli>=2.0.1 ; extra == 'toml'
+ - pyyaml>=6.0.1 ; extra == 'yaml'
+ requires_python: '>=3.10'
- pypi: https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl
name: pydeck
version: 0.9.1
@@ -7047,10 +7281,10 @@ packages:
version: 0.2.1
sha256: 5b26fc6e056212e402cf3e6338165ac16e1b19c000f882894d21f2de5bf79af0
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl
name: pyparsing
- version: 3.3.1
- sha256: 023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82
+ version: 3.3.2
+ sha256: 850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d
requires_dist:
- railroad-diagrams ; extra == 'diagrams'
- jinja2 ; extra == 'diagrams'
@@ -7097,6 +7331,55 @@ packages:
- pystac[validation]>=1.10.0
- python-dateutil>=2.8.2
requires_python: '>=3.10'
+- pypi: https://files.pythonhosted.org/packages/0d/48/75c0c38ea2b086cd967b7241f394773437a1a7c09dc9b178b49f04399207/pytabkit-1.7.3-py3-none-any.whl
+ name: pytabkit
+ version: 1.7.3
+ sha256: 1589f281e99d4a6965a83f954d9f78b038fc633a4246d450a1a3599952d4d841
+ requires_dist:
+ - numpy>=1.25
+ - pandas>=2.0
+ - psutil>=5.0
+ - pytorch-lightning>=2.0
+ - scikit-learn>=1.3
+ - torch>=2.0
+ - torchmetrics>=1.2.1
+ - autogluon-multimodal>=1.0 ; extra == 'autogluon'
+ - autogluon-tabular[all]>=1.0 ; extra == 'autogluon'
+ - adjusttext>=1.0 ; extra == 'bench'
+ - autorank>=1.0 ; extra == 'bench'
+ - fire ; extra == 'bench'
+ - matplotlib>=3.0 ; extra == 'bench'
+ - openml>=0.14 ; extra == 'bench'
+ - openpyxl>=3.0 ; extra == 'bench'
+ - patool>=1.0 ; extra == 'bench'
+ - ray>=2.8 ; extra == 'bench'
+ - requests>=2.0 ; extra == 'bench'
+ - seaborn>=0.0.13 ; extra == 'bench'
+ - tueplots>=0.0.12 ; extra == 'bench'
+ - xlrd>=2.0 ; extra == 'bench'
+ - myst-parser>=3.0 ; extra == 'dev'
+ - pytest-cov>=4.0 ; extra == 'dev'
+ - pytest>=7.0 ; extra == 'dev'
+ - sphinx-rtd-theme>=2.0 ; extra == 'dev'
+ - sphinx>=7.0 ; extra == 'dev'
+ - kditransform>=0.2 ; extra == 'extra'
+ - configspace>=0.7 ; extra == 'hpo'
+ - hyperopt>=0.2 ; extra == 'hpo'
+ - smac>=2.0 ; extra == 'hpo'
+ - catboost>=1.2 ; extra == 'models'
+ - dask[dataframe]>=2023 ; extra == 'models'
+ - dill ; extra == 'models'
+ - lightgbm>=4.1 ; extra == 'models'
+ - msgpack>=1.0 ; extra == 'models'
+ - numba>=0.59.0 ; extra == 'models'
+ - probmetrics>=0.0.1 ; extra == 'models'
+ - pyyaml>=5.0 ; extra == 'models'
+ - skorch>=0.15 ; extra == 'models'
+ - torch>=2.0 ; extra == 'models'
+ - tqdm ; extra == 'models'
+ - xgboost>=2.0 ; extra == 'models'
+ - xrfm>=0.4.3 ; extra == 'models'
+ requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl
name: pytest
version: 9.0.2
@@ -7180,10 +7463,10 @@ packages:
requires_dist:
- click>=5.0 ; extra == 'cli'
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/f7/1b/354a0ab669703f87e9ab0464670be8791a9de59c2693ffb0d9a584927b5e/python_fasthtml-0.12.39-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/22/da/e890190fd9b3bd465c796a341fb24cbb1b0f9d831f511d21db9faf056e32/python_fasthtml-0.12.41-py3-none-any.whl
name: python-fasthtml
- version: 0.12.39
- sha256: d9d2a173714852f906d1821f5eeb40640db2ef2b2f997edee63471feb58127be
+ version: 0.12.41
+ sha256: fc0a1b7735bfd591076b5dd7b6af8050e76ba1604fa5ad9d250aec6f5a69af93
requires_dist:
- fastcore>=1.10.0
- python-dateutil
@@ -7201,10 +7484,10 @@ packages:
- monsterui ; extra == 'dev'
- pyjwt ; extra == 'dev'
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl
name: python-multipart
- version: 0.0.21
- sha256: cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090
+ version: 0.0.22
+ sha256: 2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155
requires_python: '>=3.10'
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.3-pyhd8ed1ab_0.conda
sha256: 467134ef39f0af2dbb57d78cb3e4821f01003488d331a8dd7119334f4f47bfbd
@@ -7298,6 +7581,101 @@ packages:
purls: []
size: 50240
timestamp: 1764982604740
+- pypi: https://files.pythonhosted.org/packages/0e/93/c8c361bf0a2fe50f828f32def460e8b8a14b93955d3fd302b1a9b63b19e4/pytorch_lightning-2.6.1-py3-none-any.whl
+ name: pytorch-lightning
+ version: 2.6.1
+ sha256: 1f8118567ec829e3055f16cf1aa320883a86a47c836951bfd9dcfa34ec7ffd59
+ requires_dist:
+ - torch>=2.1.0
+ - tqdm>=4.57.0
+ - pyyaml>5.4
+ - fsspec[http]>=2022.5.0
+ - torchmetrics>0.7.0
+ - packaging>=23.0
+ - typing-extensions>4.5.0
+ - lightning-utilities>=0.10.0
+ - matplotlib>3.1 ; extra == 'extra'
+ - omegaconf>=2.2.3 ; extra == 'extra'
+ - hydra-core>=1.2.0 ; extra == 'extra'
+ - jsonargparse[jsonnet,signatures]>=4.39.0 ; extra == 'extra'
+ - rich>=12.3.0 ; extra == 'extra'
+ - tensorboardx>=2.2 ; extra == 'extra'
+ - bitsandbytes>=0.45.2 ; sys_platform != 'darwin' and extra == 'extra'
+ - deepspeed>=0.15.0,<0.17.0 ; sys_platform != 'darwin' and sys_platform != 'win32' and extra == 'strategies'
+ - coverage==7.13.1 ; python_full_version >= '3.10' and extra == 'test'
+ - coverage==7.10.7 ; python_full_version < '3.10' and extra == 'test'
+ - pytest==9.0.2 ; extra == 'test'
+ - pytest-cov==7.0.0 ; extra == 'test'
+ - pytest-timeout==2.4.0 ; extra == 'test'
+ - pytest-rerunfailures==16.0.1 ; python_full_version < '3.10' and extra == 'test'
+ - pytest-rerunfailures==16.1 ; python_full_version >= '3.10' and extra == 'test'
+ - pytest-random-order==1.2.0 ; extra == 'test'
+ - cloudpickle>=1.3 ; extra == 'test'
+ - scikit-learn>0.22.1 ; extra == 'test'
+ - numpy>1.21.0 ; python_full_version < '3.12' and extra == 'test'
+ - numpy>2.1.0 ; python_full_version >= '3.12' and extra == 'test'
+ - onnx>1.12.0 ; extra == 'test'
+ - onnxruntime>=1.12.0 ; extra == 'test'
+ - onnxscript>=0.1.0 ; extra == 'test'
+ - psutil<7.3.0 ; extra == 'test'
+ - pandas>2.0 ; extra == 'test'
+ - fastapi ; extra == 'test'
+ - uvicorn ; extra == 'test'
+ - tensorboard>=2.11 ; extra == 'test'
+ - torch-tensorrt ; python_full_version >= '3.12' and sys_platform != 'darwin' and extra == 'test'
+ - huggingface-hub ; extra == 'test'
+ - requests<2.33.0 ; extra == 'examples'
+ - torchvision>=0.16.0 ; extra == 'examples'
+ - ipython[all]>=8.0.0 ; extra == 'examples'
+ - torchmetrics>=0.10.0 ; extra == 'examples'
+ - deepspeed>=0.15.0,<0.17.0 ; sys_platform != 'darwin' and sys_platform != 'win32' and extra == 'deepspeed'
+ - matplotlib>3.1 ; extra == 'all'
+ - omegaconf>=2.2.3 ; extra == 'all'
+ - hydra-core>=1.2.0 ; extra == 'all'
+ - jsonargparse[jsonnet,signatures]>=4.39.0 ; extra == 'all'
+ - rich>=12.3.0 ; extra == 'all'
+ - tensorboardx>=2.2 ; extra == 'all'
+ - bitsandbytes>=0.45.2 ; sys_platform != 'darwin' and extra == 'all'
+ - deepspeed>=0.15.0,<0.17.0 ; sys_platform != 'darwin' and sys_platform != 'win32' and extra == 'all'
+ - requests<2.33.0 ; extra == 'all'
+ - torchvision>=0.16.0 ; extra == 'all'
+ - ipython[all]>=8.0.0 ; extra == 'all'
+ - torchmetrics>=0.10.0 ; extra == 'all'
+ - matplotlib>3.1 ; extra == 'dev'
+ - omegaconf>=2.2.3 ; extra == 'dev'
+ - hydra-core>=1.2.0 ; extra == 'dev'
+ - jsonargparse[jsonnet,signatures]>=4.39.0 ; extra == 'dev'
+ - rich>=12.3.0 ; extra == 'dev'
+ - tensorboardx>=2.2 ; extra == 'dev'
+ - bitsandbytes>=0.45.2 ; sys_platform != 'darwin' and extra == 'dev'
+ - deepspeed>=0.15.0,<0.17.0 ; sys_platform != 'darwin' and sys_platform != 'win32' and extra == 'dev'
+ - requests<2.33.0 ; extra == 'dev'
+ - torchvision>=0.16.0 ; extra == 'dev'
+ - ipython[all]>=8.0.0 ; extra == 'dev'
+ - torchmetrics>=0.10.0 ; extra == 'dev'
+ - coverage==7.13.1 ; python_full_version >= '3.10' and extra == 'dev'
+ - coverage==7.10.7 ; python_full_version < '3.10' and extra == 'dev'
+ - pytest==9.0.2 ; extra == 'dev'
+ - pytest-cov==7.0.0 ; extra == 'dev'
+ - pytest-timeout==2.4.0 ; extra == 'dev'
+ - pytest-rerunfailures==16.0.1 ; python_full_version < '3.10' and extra == 'dev'
+ - pytest-rerunfailures==16.1 ; python_full_version >= '3.10' and extra == 'dev'
+ - pytest-random-order==1.2.0 ; extra == 'dev'
+ - cloudpickle>=1.3 ; extra == 'dev'
+ - scikit-learn>0.22.1 ; extra == 'dev'
+ - numpy>1.21.0 ; python_full_version < '3.12' and extra == 'dev'
+ - numpy>2.1.0 ; python_full_version >= '3.12' and extra == 'dev'
+ - onnx>1.12.0 ; extra == 'dev'
+ - onnxruntime>=1.12.0 ; extra == 'dev'
+ - onnxscript>=0.1.0 ; extra == 'dev'
+ - psutil<7.3.0 ; extra == 'dev'
+ - pandas>2.0 ; extra == 'dev'
+ - fastapi ; extra == 'dev'
+ - uvicorn ; extra == 'dev'
+ - tensorboard>=2.11 ; extra == 'dev'
+ - torch-tensorrt ; python_full_version >= '3.12' and sys_platform != 'darwin' and extra == 'dev'
+ - huggingface-hub ; extra == 'dev'
+ requires_python: '>=3.10'
- conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda
sha256: 8d2a8bf110cc1fc3df6904091dead158ba3e614d8402a83e51ed3a8aa93cdeb0
md5: bc8e3267d44011051f2eb14d22fb0960
@@ -7692,10 +8070,10 @@ packages:
- rpds-py>=0.7.0
- typing-extensions>=4.4.0 ; python_full_version < '3.13'
requires_python: '>=3.10'
-- pypi: https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
name: regex
- version: 2025.11.3
- sha256: 4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850
+ version: 2026.1.15
+ sha256: 18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6
requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl
name: requests
@@ -7732,14 +8110,14 @@ packages:
- docutils
- rich>=12.0.0
- sphinx ; extra == 'docs'
-- pypi: https://files.pythonhosted.org/packages/d6/e5/4f4fc949e7eb8415a57091767969e1d314dcf06b74b85bbbf29991395af4/rioxarray-0.20.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/62/aa/4ba81b24acf2b05cb00fd7603f84e5088af72596839f85c23c6ff5d249d7/rioxarray-0.21.0-py3-none-any.whl
name: rioxarray
- version: 0.20.0
- sha256: 197b0638146dfc6093ef52f8bf8afb42757ca16bc2e0d87b6282ce54170c9799
+ version: 0.21.0
+ sha256: f8793822f09ab7de3a82ffe711beff4cdf313a860ff86df0942f3385374ac562
requires_dist:
- packaging
- rasterio>=1.4.3
- - xarray>=2024.7.0
+ - xarray>=2024.7.0,<2025.12
- pyproj>=3.3
- numpy>=2
- scipy ; extra == 'interp'
@@ -7775,10 +8153,10 @@ packages:
requires_dist:
- pyasn1>=0.1.3
requires_python: '>=3.6,<4'
-- pypi: https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
name: ruff
- version: 0.14.11
- sha256: a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3
+ version: 0.14.14
+ sha256: bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974
requires_python: '>=3.7'
- conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda
sha256: dec76e9faa3173579d34d226dbc91892417a80784911daf8e3f0eb9bad19d7a6
@@ -7932,6 +8310,66 @@ packages:
- scipy>=1.7 ; extra == 'stats'
- statsmodels>=0.12 ; extra == 'stats'
requires_python: '>=3.8'
+- pypi: https://files.pythonhosted.org/packages/ca/63/2c6daf59d86b1c30600bff679d039f57fd1932af82c43c0bde1cbc55e8d4/sentry_sdk-2.52.0-py2.py3-none-any.whl
+ name: sentry-sdk
+ version: 2.52.0
+ sha256: 931c8f86169fc6f2752cb5c4e6480f0d516112e78750c312e081ababecbaf2ed
+ requires_dist:
+ - urllib3>=1.26.11
+ - certifi
+ - aiohttp>=3.5 ; extra == 'aiohttp'
+ - anthropic>=0.16 ; extra == 'anthropic'
+ - arq>=0.23 ; extra == 'arq'
+ - asyncpg>=0.23 ; extra == 'asyncpg'
+ - apache-beam>=2.12 ; extra == 'beam'
+ - bottle>=0.12.13 ; extra == 'bottle'
+ - celery>=3 ; extra == 'celery'
+ - celery-redbeat>=2 ; extra == 'celery-redbeat'
+ - chalice>=1.16.0 ; extra == 'chalice'
+ - clickhouse-driver>=0.2.0 ; extra == 'clickhouse-driver'
+ - django>=1.8 ; extra == 'django'
+ - falcon>=1.4 ; extra == 'falcon'
+ - fastapi>=0.79.0 ; extra == 'fastapi'
+ - flask>=0.11 ; extra == 'flask'
+ - blinker>=1.1 ; extra == 'flask'
+ - markupsafe ; extra == 'flask'
+ - grpcio>=1.21.1 ; extra == 'grpcio'
+ - protobuf>=3.8.0 ; extra == 'grpcio'
+ - httpcore[http2]==1.* ; extra == 'http2'
+ - httpx>=0.16.0 ; extra == 'httpx'
+ - huey>=2 ; extra == 'huey'
+ - huggingface-hub>=0.22 ; extra == 'huggingface-hub'
+ - langchain>=0.0.210 ; extra == 'langchain'
+ - langgraph>=0.6.6 ; extra == 'langgraph'
+ - launchdarkly-server-sdk>=9.8.0 ; extra == 'launchdarkly'
+ - litellm>=1.77.5 ; extra == 'litellm'
+ - litestar>=2.0.0 ; extra == 'litestar'
+ - loguru>=0.5 ; extra == 'loguru'
+ - mcp>=1.15.0 ; extra == 'mcp'
+ - openai>=1.0.0 ; extra == 'openai'
+ - tiktoken>=0.3.0 ; extra == 'openai'
+ - openfeature-sdk>=0.7.1 ; extra == 'openfeature'
+ - opentelemetry-distro>=0.35b0 ; extra == 'opentelemetry'
+ - opentelemetry-distro ; extra == 'opentelemetry-experimental'
+ - opentelemetry-distro[otlp]>=0.35b0 ; extra == 'opentelemetry-otlp'
+ - pure-eval ; extra == 'pure-eval'
+ - executing ; extra == 'pure-eval'
+ - asttokens ; extra == 'pure-eval'
+ - pydantic-ai>=1.0.0 ; extra == 'pydantic-ai'
+ - pymongo>=3.1 ; extra == 'pymongo'
+ - pyspark>=2.4.4 ; extra == 'pyspark'
+ - quart>=0.16.1 ; extra == 'quart'
+ - blinker>=1.1 ; extra == 'quart'
+ - rq>=0.6 ; extra == 'rq'
+ - sanic>=0.8 ; extra == 'sanic'
+ - sqlalchemy>=1.2 ; extra == 'sqlalchemy'
+ - starlette>=0.19.1 ; extra == 'starlette'
+ - starlite>=1.48 ; extra == 'starlite'
+ - statsig>=0.55.3 ; extra == 'statsig'
+ - tornado>=6 ; extra == 'tornado'
+ - unleashclient>=6.0.1 ; extra == 'unleash'
+ - google-genai>=1.29.0 ; extra == 'google-genai'
+ requires_python: '>=3.6'
- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda
sha256: 972560fcf9657058e3e1f97186cc94389144b46dbdf58c807ce62e83f977e863
md5: 4de79c071274a53dcaf2a8c749d1499e
@@ -8136,10 +8574,10 @@ packages:
name: sortedcontainers
version: 2.4.0
sha256: a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
-- pypi: https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl
name: soupsieve
- version: 2.8.1
- sha256: a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434
+ version: 2.8.3
+ sha256: ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95
requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/c6/1f/731beb48f2c7415a71e2f655876fea8a0b3a6798be3d4d51b794f939623d/spacy-3.8.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
name: spacy
@@ -8234,10 +8672,10 @@ packages:
- pygments ; extra == 'tests'
- littleutils ; extra == 'tests'
- cython ; extra == 'tests'
-- pypi: https://files.pythonhosted.org/packages/18/c4/09985a03dba389d4fe16a9014147a7b02fa76ef3519bf5846462a485876d/starlette-0.51.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl
name: starlette
- version: 0.51.0
- sha256: fb460a3d6fd3c958d729fdd96aee297f89a51b0181f16401fe8fd4cb6129165d
+ version: 0.52.1
+ sha256: 0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74
requires_dist:
- anyio>=3.6.2,<5
- typing-extensions>=4.10.0 ; python_full_version < '3.13'
@@ -8261,31 +8699,37 @@ packages:
requires_dist:
- pandas>=2.2.3 ; extra == 'export'
requires_python: '>=3.11'
-- pypi: https://files.pythonhosted.org/packages/c0/95/6b7873f0267973ebd55ba9cd33a690b35a116f2779901ef6185a0e21864d/streamlit-1.52.2-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/48/1d/40de1819374b4f0507411a60f4d2de0d620a9b10c817de5925799132b6c9/streamlit-1.54.0-py3-none-any.whl
name: streamlit
- version: 1.52.2
- sha256: a16bb4fbc9781e173ce9dfbd8ffb189c174f148f9ca4fb8fa56423e84e193fc8
+ version: 1.54.0
+ sha256: a7b67d6293a9f5f6b4d4c7acdbc4980d7d9f049e78e404125022ecb1712f79fc
requires_dist:
- altair>=4.0,!=5.4.0,!=5.4.1,<7
- blinker>=1.5.0,<2
- - cachetools>=4.0,<7
+ - cachetools>=5.5,<7
- click>=7.0,<9
+ - gitpython>=3.0.7,!=3.1.19,<4
- numpy>=1.23,<3
- packaging>=20
- pandas>=1.4.0,<3
- pillow>=7.1.0,<13
+ - pydeck>=0.8.0b4,<1
- protobuf>=3.20,<7
- pyarrow>=7.0
- requests>=2.27,<3
- tenacity>=8.1.0,<10
- toml>=0.10.1,<2
- - typing-extensions>=4.4.0,<5
- - watchdog>=2.1.5,<7 ; sys_platform != 'darwin'
- - gitpython>=3.0.7,!=3.1.19,<4
- - pydeck>=0.8.0b4,<1
- tornado>=6.0.3,!=6.5.0,<7
+ - typing-extensions>=4.10.0,<5
+ - watchdog>=2.1.5,<7 ; sys_platform != 'darwin'
- snowflake-snowpark-python[modin]>=1.17.0 ; python_full_version < '3.12' and extra == 'snowflake'
- snowflake-connector-python>=3.3.0 ; python_full_version < '3.12' and extra == 'snowflake'
+ - starlette>=0.40.0 ; extra == 'starlette'
+ - uvicorn>=0.30.0 ; extra == 'starlette'
+ - anyio>=4.0.0 ; extra == 'starlette'
+ - python-multipart>=0.0.10 ; extra == 'starlette'
+ - websockets>=12.0.0 ; extra == 'starlette'
+ - itsdangerous>=2.1.2 ; extra == 'starlette'
- streamlit-pdf>=1.0.0 ; extra == 'pdf'
- authlib>=1.3.2 ; extra == 'auth'
- matplotlib>=3.0.0 ; extra == 'charts'
@@ -8295,6 +8739,7 @@ packages:
- sqlalchemy>=2.0.0 ; extra == 'sql'
- orjson>=3.5.0 ; extra == 'performance'
- uvloop>=0.15.2 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' and extra == 'performance'
+ - httptools>=0.6.3 ; extra == 'performance'
- streamlit[auth,charts,pdf,performance,snowflake,sql] ; extra == 'all'
- rich>=11.0.0 ; extra == 'all'
requires_python: '>=3.10'
@@ -8323,6 +8768,92 @@ packages:
- pkg:pypi/sympy?source=hash-mapping
size: 4616621
timestamp: 1745946173026
+- pypi: https://files.pythonhosted.org/packages/f0/ec/23e60c7c3f4d5247cac66a8d79f2f0070f4ef90f11570ccdb00e4d69da05/tabdpt-1.1.12-py3-none-any.whl
+ name: tabdpt
+ version: 1.1.12
+ sha256: bb6a061d85e4fb08be3d4f64293d59e71a079c8e679109489c7cf866f67f467d
+ requires_dist:
+ - faiss-cpu>=1.11.0,<1.13.0
+ - huggingface-hub>=0.33.2,<2.0
+ - numpy>=1.25.0,<3.0
+ - omegaconf>=2.1.1,<3.0
+ - safetensors>=0.5.3,<1.0
+ - scikit-learn>=1.4.0,<2.0
+ - scipy>=1.9.0,<2.0
+ - torch>=2.6.0,<3.0
+ - tqdm>=4.38.0,<5.0
+ - faiss-cpu==1.11.0 ; extra == 'reproduce-results'
+ - numpy==2.3.0 ; python_full_version >= '3.11' and extra == 'reproduce-results'
+ - omegaconf==2.3.0 ; extra == 'reproduce-results'
+ - scikit-learn==1.7.0 ; extra == 'reproduce-results'
+ - scipy==1.15.3 ; extra == 'reproduce-results'
+ - torch==2.7.1 ; extra == 'reproduce-results'
+ - rliable==1.2.0 ; extra == 'reproduce-results'
+ - pandas==2.3.2 ; extra == 'reproduce-results'
+ - openml==0.15.1 ; extra == 'reproduce-results'
+ - jupyter==1.1.1 ; extra == 'reproduce-results'
+ - tenacity==9.1.2 ; extra == 'reproduce-results'
+ - gdown==5.2.0 ; extra == 'reproduce-results'
+ requires_python: '>=3.10'
+- pypi: https://files.pythonhosted.org/packages/c9/56/bbac0d5076acbca1075177c0a40c829a497fc26598fc90fa55c73d38288a/tabicl-0.1.4-py3-none-any.whl
+ name: tabicl
+ version: 0.1.4
+ sha256: 10dcaec46e9e2ba37adb3c1c386f53253f210bbc2894af2e6d0506131c0abdcc
+ requires_dist:
+ - einops>=0.7
+ - huggingface-hub
+ - joblib
+ - numpy
+ - psutil
+ - scikit-learn>=1.3.0
+ - scipy
+ - torch>=2.2,<3
+ - tqdm>=4.64.0
+ - transformers
+ - wandb
+ - xgboost
+ requires_python: '>=3.9,<3.14'
+- pypi: https://files.pythonhosted.org/packages/a8/db/db2e7c3cd98499f313ab59021a01124f37f01d08a96daf563e803cfd5ab7/tabpfn-6.2.0-py3-none-any.whl
+ name: tabpfn
+ version: 6.2.0
+ sha256: aa3de1c30c86a0a0ac4147c4b6ce79ed12a0ad6facccff41f9927db17407ed96
+ requires_dist:
+ - torch>=2.1,<3
+ - numpy>=1.21.6,<3
+ - scikit-learn>=1.2.0,<1.8
+ - typing-extensions>=4.12.0
+ - scipy>=1.11.1,<2
+ - pandas>=1.4.0,<3
+ - einops>=0.2.0,<0.9
+ - huggingface-hub>=0.19.0,<2
+ - pydantic>=2.8.0
+ - pydantic-settings>=2.10.1
+ - eval-type-backport>=0.2.2
+ - joblib>=1.2.0
+ - tabpfn-common-utils[telemetry-interactive]>=0.2.8
+ requires_python: '>=3.9'
+- pypi: https://files.pythonhosted.org/packages/34/63/67c2453f4a30a969750c5c4296caa3caaf07e363dee617f316d1380ee3d0/tabpfn_common_utils-0.2.15-py3-none-any.whl
+ name: tabpfn-common-utils
+ version: 0.2.15
+ sha256: 5be865992d0bf3aac5afb591bdd2e67c38602d53f6e262498d56e736d930aa1e
+ requires_dist:
+ - filelock>=3.19.1
+ - numpy>=1.21.6
+ - pandas>=1.4.0,<3
+ - platformdirs>=4
+ - posthog~=6.7
+ - requests>=2.32.5
+ - ruff>=0.11.6
+ - scikit-learn>=1.2.0
+ - typing-extensions>=4.12,<5
+ - build>=1.0.0 ; extra == 'build'
+ - hatchling>=1.25 ; extra == 'build'
+ - twine>=5.0.0 ; extra == 'build'
+ - pyright>=1.1.399 ; extra == 'dev'
+ - pytest-cov>=5 ; extra == 'dev'
+ - pytest>=8.3 ; extra == 'dev'
+ - ruff>=0.9 ; extra == 'dev'
+ requires_python: '>=3.9'
- conda: https://conda.anaconda.org/conda-forge/linux-64/tbb-2022.3.0-h8d10470_1.conda
sha256: 2e3238234ae094d5a5f7c559410ea8875351b6bac0d9d0e576bf64b732b8029e
md5: e3259be3341da4bc06c5b7a78c8bf1bd
@@ -8341,17 +8872,17 @@ packages:
version: 3.2.2
sha256: 26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl
name: tenacity
- version: 9.1.2
- sha256: f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138
+ version: 9.1.4
+ sha256: 6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55
requires_dist:
- reno ; extra == 'doc'
- sphinx ; extra == 'doc'
- pytest ; extra == 'test'
- tornado>=4.5 ; extra == 'test'
- typeguard ; extra == 'test'
- requires_python: '>=3.9'
+ requires_python: '>=3.10'
- pypi: https://files.pythonhosted.org/packages/e0/1d/b5d63f1a6b824282b57f7b581810d20b7a28ca951f2d5b59f1eb0782c12b/tensorboardx-2.6.4-py3-none-any.whl
name: tensorboardx
version: 2.6.4
@@ -8361,10 +8892,10 @@ packages:
- packaging
- protobuf>=3.20
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/a9/f4/48e4a4c77ab7eea48d3b0a77f8dea0be101c83421abc64da0888c77c47cf/textual-7.1.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/9c/78/96ddb99933e11d91bc6e05edae23d2687e44213066bcbaca338898c73c47/textual-7.5.0-py3-none-any.whl
name: textual
- version: 7.1.0
- sha256: 9209dd0d1d958316832f7e59328f3911112f8e951abef7c3fbe54effd4e4caed
+ version: 7.5.0
+ sha256: 849dfee9d705eab3b2d07b33152b7bd74fb1f5056e002873cc448bce500c6374
requires_dist:
- markdown-it-py[linkify]>=2.1.0
- mdit-py-plugins
@@ -8445,10 +8976,10 @@ packages:
- pkg:pypi/threadpoolctl?source=hash-mapping
size: 23869
timestamp: 1741878358548
-- pypi: https://files.pythonhosted.org/packages/1b/fe/e59859aa1134fac065d36864752daf13215c98b379cb5d93f954dc0ec830/tifffile-2025.12.20-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/09/19/529b28ca338c5a88315e71e672badc85eef89460c248c4164f6ce058f8c7/tifffile-2026.1.28-py3-none-any.whl
name: tifffile
- version: 2025.12.20
- sha256: bc0345a20675149353cfcb3f1c48d0a3654231ee26bd46beebaab4d2168feeb6
+ version: 2026.1.28
+ sha256: 45b08a19cf603dd99952eff54a61519626a1912e4e2a4d355f05938fe4a6e9fd
requires_dist:
- numpy
- imagecodecs>=2025.11.11 ; extra == 'codecs'
@@ -8525,6 +9056,156 @@ packages:
version: 1.1.0
sha256: 15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8
requires_python: '>=3.9'
+- pypi: https://files.pythonhosted.org/packages/02/21/aa0f434434c48490f91b65962b1ce863fdcce63febc166ca9fe9d706c2b6/torchmetrics-1.8.2-py3-none-any.whl
+ name: torchmetrics
+ version: 1.8.2
+ sha256: 08382fd96b923e39e904c4d570f3d49e2cc71ccabd2a94e0f895d1f0dac86242
+ requires_dist:
+ - numpy>1.20.0
+ - packaging>17.1
+ - torch>=2.0.0
+ - lightning-utilities>=0.8.0
+ - onnxruntime>=1.12.0 ; extra == 'audio'
+ - requests>=2.19.0 ; extra == 'audio'
+ - torchaudio>=2.0.1 ; extra == 'audio'
+ - gammatone>=1.0.0 ; extra == 'audio'
+ - pystoi>=0.4.0 ; extra == 'audio'
+ - pesq>=0.0.4 ; extra == 'audio'
+ - librosa>=0.10.0 ; extra == 'audio'
+ - torch-linear-assignment>=0.0.2 ; extra == 'clustering'
+ - pycocotools>2.0.0 ; extra == 'detection'
+ - torchvision>=0.15.1 ; extra == 'detection'
+ - torch-fidelity<=0.4.0 ; extra == 'image'
+ - torchvision>=0.15.1 ; extra == 'image'
+ - scipy>1.0.0 ; extra == 'image'
+ - piq<=0.8.0 ; extra == 'multimodal'
+ - einops>=0.7.0 ; extra == 'multimodal'
+ - transformers>=4.43.0 ; extra == 'multimodal'
+ - timm>=0.9.0 ; extra == 'multimodal'
+ - transformers>=4.43.0 ; extra == 'text'
+ - regex>=2021.9.24 ; extra == 'text'
+ - sentencepiece>=0.2.0 ; extra == 'text'
+ - nltk>3.8.1 ; extra == 'text'
+ - tqdm<4.68.0 ; extra == 'text'
+ - mecab-python3>=1.0.6 ; extra == 'text'
+ - ipadic>=1.0.0 ; extra == 'text'
+ - mypy==1.17.1 ; extra == 'typing'
+ - types-six ; extra == 'typing'
+ - torch==2.8.0 ; extra == 'typing'
+ - types-emoji ; extra == 'typing'
+ - types-protobuf ; extra == 'typing'
+ - types-setuptools ; extra == 'typing'
+ - types-requests ; extra == 'typing'
+ - types-tabulate ; extra == 'typing'
+ - types-pyyaml ; extra == 'typing'
+ - einops>=0.7.0 ; extra == 'video'
+ - vmaf-torch>=1.1.0 ; extra == 'video'
+ - scienceplots>=2.0.0 ; extra == 'visual'
+ - matplotlib>=3.6.0 ; extra == 'visual'
+ - onnxruntime>=1.12.0 ; extra == 'all'
+ - requests>=2.19.0 ; extra == 'all'
+ - torchaudio>=2.0.1 ; extra == 'all'
+ - gammatone>=1.0.0 ; extra == 'all'
+ - pystoi>=0.4.0 ; extra == 'all'
+ - pesq>=0.0.4 ; extra == 'all'
+ - librosa>=0.10.0 ; extra == 'all'
+ - torch-linear-assignment>=0.0.2 ; extra == 'all'
+ - pycocotools>2.0.0 ; extra == 'all'
+ - torchvision>=0.15.1 ; extra == 'all'
+ - torch-fidelity<=0.4.0 ; extra == 'all'
+ - torchvision>=0.15.1 ; extra == 'all'
+ - scipy>1.0.0 ; extra == 'all'
+ - piq<=0.8.0 ; extra == 'all'
+ - einops>=0.7.0 ; extra == 'all'
+ - transformers>=4.43.0 ; extra == 'all'
+ - timm>=0.9.0 ; extra == 'all'
+ - transformers>=4.43.0 ; extra == 'all'
+ - regex>=2021.9.24 ; extra == 'all'
+ - sentencepiece>=0.2.0 ; extra == 'all'
+ - nltk>3.8.1 ; extra == 'all'
+ - tqdm<4.68.0 ; extra == 'all'
+ - mecab-python3>=1.0.6 ; extra == 'all'
+ - ipadic>=1.0.0 ; extra == 'all'
+ - mypy==1.17.1 ; extra == 'all'
+ - types-six ; extra == 'all'
+ - torch==2.8.0 ; extra == 'all'
+ - types-emoji ; extra == 'all'
+ - types-protobuf ; extra == 'all'
+ - types-setuptools ; extra == 'all'
+ - types-requests ; extra == 'all'
+ - types-tabulate ; extra == 'all'
+ - types-pyyaml ; extra == 'all'
+ - einops>=0.7.0 ; extra == 'all'
+ - vmaf-torch>=1.1.0 ; extra == 'all'
+ - scienceplots>=2.0.0 ; extra == 'all'
+ - matplotlib>=3.6.0 ; extra == 'all'
+ - onnxruntime>=1.12.0 ; extra == 'dev'
+ - requests>=2.19.0 ; extra == 'dev'
+ - torchaudio>=2.0.1 ; extra == 'dev'
+ - gammatone>=1.0.0 ; extra == 'dev'
+ - pystoi>=0.4.0 ; extra == 'dev'
+ - pesq>=0.0.4 ; extra == 'dev'
+ - librosa>=0.10.0 ; extra == 'dev'
+ - torch-linear-assignment>=0.0.2 ; extra == 'dev'
+ - pycocotools>2.0.0 ; extra == 'dev'
+ - torchvision>=0.15.1 ; extra == 'dev'
+ - torch-fidelity<=0.4.0 ; extra == 'dev'
+ - torchvision>=0.15.1 ; extra == 'dev'
+ - scipy>1.0.0 ; extra == 'dev'
+ - piq<=0.8.0 ; extra == 'dev'
+ - einops>=0.7.0 ; extra == 'dev'
+ - transformers>=4.43.0 ; extra == 'dev'
+ - timm>=0.9.0 ; extra == 'dev'
+ - transformers>=4.43.0 ; extra == 'dev'
+ - regex>=2021.9.24 ; extra == 'dev'
+ - sentencepiece>=0.2.0 ; extra == 'dev'
+ - nltk>3.8.1 ; extra == 'dev'
+ - tqdm<4.68.0 ; extra == 'dev'
+ - mecab-python3>=1.0.6 ; extra == 'dev'
+ - ipadic>=1.0.0 ; extra == 'dev'
+ - mypy==1.17.1 ; extra == 'dev'
+ - types-six ; extra == 'dev'
+ - torch==2.8.0 ; extra == 'dev'
+ - types-emoji ; extra == 'dev'
+ - types-protobuf ; extra == 'dev'
+ - types-setuptools ; extra == 'dev'
+ - types-requests ; extra == 'dev'
+ - types-tabulate ; extra == 'dev'
+ - types-pyyaml ; extra == 'dev'
+ - einops>=0.7.0 ; extra == 'dev'
+ - vmaf-torch>=1.1.0 ; extra == 'dev'
+ - scienceplots>=2.0.0 ; extra == 'dev'
+ - matplotlib>=3.6.0 ; extra == 'dev'
+ - properscoring==0.1 ; extra == 'dev'
+ - mir-eval>=0.6 ; extra == 'dev'
+ - pytorch-msssim==1.0.0 ; extra == 'dev'
+ - scikit-image>=0.19.0 ; extra == 'dev'
+ - sacrebleu>=2.3.0 ; extra == 'dev'
+ - dists-pytorch==0.1 ; extra == 'dev'
+ - torch-complex<0.5.0 ; extra == 'dev'
+ - pytdc==0.4.1 ; (python_full_version < '3.10' and extra == 'dev') or (python_full_version < '3.12' and sys_platform == 'win32' and extra == 'dev')
+ - netcal>1.0.0 ; extra == 'dev'
+ - lpips<=0.1.4 ; extra == 'dev'
+ - jiwer>=2.3.0 ; extra == 'dev'
+ - fairlearn ; extra == 'dev'
+ - monai==1.4.0 ; extra == 'dev'
+ - statsmodels>0.13.5 ; extra == 'dev'
+ - mecab-ko-dic>=1.0.0 ; python_full_version < '3.12' and extra == 'dev'
+ - sewar>=0.4.4 ; extra == 'dev'
+ - mecab-ko>=1.0.0,<1.1.0 ; python_full_version < '3.12' and extra == 'dev'
+ - faster-coco-eval>=1.6.3 ; extra == 'dev'
+ - huggingface-hub<0.35 ; extra == 'dev'
+ - numpy<2.4.0 ; extra == 'dev'
+ - permetrics==2.0.0 ; extra == 'dev'
+ - bert-score==0.3.13 ; extra == 'dev'
+ - scipy>1.0.0 ; extra == 'dev'
+ - kornia>=0.6.7 ; extra == 'dev'
+ - rouge-score>0.1.0 ; extra == 'dev'
+ - fast-bss-eval>=0.1.0 ; extra == 'dev'
+ - aeon>=1.0.0 ; python_full_version >= '3.11' and extra == 'dev'
+ - pandas>1.4.0 ; extra == 'dev'
+ - dython==0.7.9 ; extra == 'dev'
+ requires_python: '>=3.9'
- pypi: https://files.pythonhosted.org/packages/10/b5/5bba24ff9d325181508501ed7f0c3de8ed3dd2edca0784d48b144b6c5252/torchvision-0.24.1-cp313-cp313-manylinux_2_28_x86_64.whl
name: torchvision
version: 0.24.1
@@ -8541,12 +9222,13 @@ packages:
version: 6.5.4
sha256: e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl
name: tqdm
- version: 4.67.1
- sha256: 26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2
+ version: 4.67.3
+ sha256: ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf
requires_dist:
- colorama ; sys_platform == 'win32'
+ - importlib-metadata ; python_full_version < '3.8'
- pytest>=6 ; extra == 'dev'
- pytest-cov ; extra == 'dev'
- pytest-timeout ; extra == 'dev'
@@ -8582,10 +9264,10 @@ packages:
- pandas ; extra == 'test'
- xarray ; extra == 'test'
- pytest ; extra == 'test'
-- pypi: https://files.pythonhosted.org/packages/6a/6b/2f416568b3c4c91c96e5a365d164f8a4a4a88030aa8ab4644181fdadce97/transformers-4.57.3-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl
name: transformers
- version: 4.57.3
- sha256: c77d353a4851b1880191603d36acb313411d3577f6e2897814f333841f7003f4
+ version: 4.57.6
+ sha256: 4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550
requires_dist:
- filelock
- huggingface-hub>=0.34.0,<1.0
@@ -9187,10 +9869,10 @@ packages:
license: BSD-3-Clause
size: 508347
timestamp: 1765407086135
-- pypi: https://files.pythonhosted.org/packages/43/6c/b26831b890b37c09882f6406efd31441c8e512bf1efbc967b9d867c5e02b/ultraplot-1.70.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/d6/32/48209716f9715d77f1bce084ad74c5d3cfcf41fd78d0c7e7dbe4829cfa3a/ultraplot-1.72.0-py3-none-any.whl
name: ultraplot
- version: 1.70.0
- sha256: 2b29d1b1e36bd6cf88458370825cfab2c62b9acab706a2cfa434660d7dc4bf74
+ version: 1.72.0
+ sha256: dc276064791661018311923bb5bf3cd541b79399682e48de1b88d8556053ae2d
requires_dist:
- numpy>=1.26.0
- matplotlib>=3.9,<3.11
@@ -9292,11 +9974,81 @@ packages:
- setuptools>=68 ; extra == 'test'
- time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test'
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/6f/61/dc6f4a38cf1b8699f64c57d7f021ca42c39bfe782d8a6eaefb7e8418e925/vl_convert_python-1.9.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/a0/18/88e02899b72fa8273ffb32bde12b0e5776ee0fd9fb29559a49c48ec4c5fa/vl_convert_python-1.9.0.post1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
name: vl-convert-python
- version: 1.9.0
- sha256: 849e6773a7e05d58ab215386b1065e7713f4846b9ac6b0d743bb3e1b20337231
+ version: 1.9.0.post1
+ sha256: 3c1558fa0055e88c465bd3d71760cde9fa2c94a95f776a0ef9178252fd820b1f
requires_python: '>=3.7'
+- pypi: https://files.pythonhosted.org/packages/70/c8/1b758bd903afee000f023cd03f335ff328a21b3914f9f9deda49b1e57723/wandb-0.24.2-py3-none-manylinux_2_28_x86_64.whl
+ name: wandb
+ version: 0.24.2
+ sha256: 38661c666e70d7e1f460fc0a0edab8a393eaaa5f8773c17be534961a7022779d
+ requires_dist:
+ - click>=8.0.1
+ - eval-type-backport ; python_full_version < '3.10'
+ - gitpython>=1.0.0,!=3.1.29
+ - packaging
+ - platformdirs
+ - protobuf>=3.12.0,!=4.21.0,!=5.28.0,<7 ; python_full_version < '3.9' and sys_platform == 'linux'
+ - protobuf>=3.15.0,!=4.21.0,!=5.28.0,<7 ; python_full_version == '3.9.*' and sys_platform == 'linux'
+ - protobuf>=3.19.0,!=4.21.0,!=5.28.0,<7 ; python_full_version >= '3.10' and sys_platform == 'linux'
+ - protobuf>=3.19.0,!=4.21.0,!=5.28.0,<7 ; sys_platform != 'linux'
+ - pydantic<3
+ - pyyaml
+ - requests>=2.0.0,<3
+ - sentry-sdk>=2.0.0
+ - typing-extensions>=4.8,<5
+ - boto3 ; extra == 'aws'
+ - botocore>=1.5.76 ; extra == 'aws'
+ - azure-identity ; extra == 'azure'
+ - azure-storage-blob ; extra == 'azure'
+ - google-cloud-storage ; extra == 'gcp'
+ - filelock ; extra == 'importers'
+ - mlflow ; extra == 'importers'
+ - polars<=1.2.1 ; extra == 'importers'
+ - rich ; extra == 'importers'
+ - tenacity ; extra == 'importers'
+ - google-cloud-storage ; extra == 'kubeflow'
+ - kubernetes ; extra == 'kubeflow'
+ - minio ; extra == 'kubeflow'
+ - sh ; extra == 'kubeflow'
+ - awscli ; extra == 'launch'
+ - azure-containerregistry ; extra == 'launch'
+ - azure-identity ; extra == 'launch'
+ - azure-storage-blob ; extra == 'launch'
+ - boto3 ; extra == 'launch'
+ - botocore>=1.5.76 ; extra == 'launch'
+ - chardet ; extra == 'launch'
+ - google-auth ; extra == 'launch'
+ - google-cloud-aiplatform ; extra == 'launch'
+ - google-cloud-artifact-registry ; extra == 'launch'
+ - google-cloud-compute ; extra == 'launch'
+ - google-cloud-storage ; extra == 'launch'
+ - iso8601 ; extra == 'launch'
+ - jsonschema ; extra == 'launch'
+ - kubernetes ; extra == 'launch'
+ - kubernetes-asyncio ; extra == 'launch'
+ - nbconvert ; extra == 'launch'
+ - nbformat ; extra == 'launch'
+ - optuna ; extra == 'launch'
+ - pydantic ; extra == 'launch'
+ - pyyaml>=6.0.0 ; extra == 'launch'
+ - tomli ; extra == 'launch'
+ - tornado>=6.5.0 ; python_full_version >= '3.9' and extra == 'launch'
+ - typing-extensions ; extra == 'launch'
+ - bokeh ; extra == 'media'
+ - imageio>=2.28.1 ; extra == 'media'
+ - moviepy>=1.0.0 ; extra == 'media'
+ - numpy ; extra == 'media'
+ - pillow ; extra == 'media'
+ - plotly>=5.18.0 ; extra == 'media'
+ - rdkit ; extra == 'media'
+ - soundfile ; extra == 'media'
+ - cloudpickle ; extra == 'models'
+ - orjson ; extra == 'perf'
+ - sweeps>=0.2.0 ; extra == 'sweeps'
+ - wandb-workspaces ; extra == 'workspaces'
+ requires_python: '>=3.8'
- pypi: https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl
name: wasabi
version: 1.1.3
@@ -9319,11 +10071,11 @@ packages:
requires_dist:
- anyio>=3.0.0
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl
name: wcwidth
- version: 0.2.14
- sha256: a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1
- requires_python: '>=3.6'
+ version: 0.6.0
+ sha256: 1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad
+ requires_python: '>=3.8'
- pypi: https://files.pythonhosted.org/packages/a4/74/a148b41572656904a39dfcfed3f84dd1066014eed94e209223ae8e9d088d/weasel-0.4.3-py3-none-any.whl
name: weasel
version: 0.4.3
@@ -9363,10 +10115,10 @@ packages:
- xarray
- imageio
requires_python: '>=3.7'
-- pypi: https://files.pythonhosted.org/packages/d5/e4/62a677feefde05b12a70a4fc9bdc8558010182a801fbcab68cb56c2b0986/xarray-2025.12.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/86/b4/cfa7aa56807dd2d9db0576c3440b3acd51bae6207338ec5610d4878e5c9b/xarray-2025.11.0-py3-none-any.whl
name: xarray
- version: 2025.12.0
- sha256: 9e77e820474dbbe4c6c2954d0da6342aa484e33adaa96ab916b15a786181e970
+ version: 2025.11.0
+ sha256: 986893b995de4a948429356a3897d78e634243c1cac242bd59d03832b9d72dd1
requires_dist:
- numpy>=1.26
- packaging>=24.1
diff --git a/pyproject.toml b/pyproject.toml
index fd9fe67..f1d5b80 100755
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -67,8 +67,10 @@ dependencies = [
"ruff>=0.14.11,<0.15",
"pandas-stubs>=2.3.3.251201,<3",
"pytest>=9.0.2,<10",
- "autogluon-tabular[all,mitra]>=1.5.0",
- "shap>=0.50.0,<0.51", "h5py>=3.15.1,<4",
+ "autogluon-tabular[all,mitra,realmlp,interpret,fastai,tabm,tabpfn,tabdpt,tabpfnmix,tabicl,skew,imodels]>=1.5.0",
+ "shap>=0.50.0,<0.51",
+ "h5py>=3.15.1,<4",
+ "pydantic>=2.12.5,<3",
]
[project.scripts]
@@ -77,8 +79,8 @@ darts = "entropice.ingest.darts:cli"
alpha-earth = "entropice.ingest.alphaearth:main"
era5 = "entropice.ingest.era5:cli"
arcticdem = "entropice.ingest.arcticdem:cli"
-train = "entropice.ml.training:cli"
-autogluon = "entropice.ml.autogluon_training:cli"
+train = "entropice.ml.hpsearchcv:cli"
+autogluon = "entropice.ml.autogluon:cli"
[build-system]
requires = ["hatchling"]
diff --git a/src/entropice/dashboard/app.py b/src/entropice/dashboard/app.py
index dd8e508..26503e6 100644
--- a/src/entropice/dashboard/app.py
+++ b/src/entropice/dashboard/app.py
@@ -5,6 +5,7 @@ Pages:
- Overview: List of available result directories with some summary statistics.
- Training Data: Visualization of training data distributions.
- Training Results Analysis: Analysis of training results and model performance.
+- Experiment Analysis: Compare multiple training runs within an experiment.
- AutoGluon Analysis: Analysis of AutoGluon training results with SHAP visualizations.
- Model State: Visualization of model state and features.
- Inference: Visualization of inference results.
@@ -13,6 +14,7 @@ Pages:
import streamlit as st
from entropice.dashboard.views.dataset_page import render_dataset_page
+from entropice.dashboard.views.experiment_analysis_page import render_experiment_analysis_page
from entropice.dashboard.views.inference_page import render_inference_page
from entropice.dashboard.views.model_state_page import render_model_state_page
from entropice.dashboard.views.overview_page import render_overview_page
@@ -27,6 +29,7 @@ def main():
overview_page = st.Page(render_overview_page, title="Overview", icon="🏡", default=True)
data_page = st.Page(render_dataset_page, title="Dataset", icon="📊")
training_analysis_page = st.Page(render_training_analysis_page, title="Training Results Analysis", icon="🦾")
+ experiment_analysis_page = st.Page(render_experiment_analysis_page, title="Experiment Analysis", icon="🔬")
model_state_page = st.Page(render_model_state_page, title="Model State", icon="🧮")
inference_page = st.Page(render_inference_page, title="Inference", icon="🗺️")
@@ -34,7 +37,7 @@ def main():
{
"Overview": [overview_page],
"Data": [data_page],
- "Experiments": [training_analysis_page, model_state_page],
+ "Experiments": [training_analysis_page, experiment_analysis_page, model_state_page],
"Inference": [inference_page],
}
)
diff --git a/src/entropice/dashboard/plots/__init__.py b/src/entropice/dashboard/plots/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/entropice/dashboard/plots/correlations.py b/src/entropice/dashboard/plots/correlations.py
new file mode 100644
index 0000000..58cb2be
--- /dev/null
+++ b/src/entropice/dashboard/plots/correlations.py
@@ -0,0 +1,619 @@
+"""Plots for cross-dataset correlation and similarity analysis."""
+
+from typing import Literal
+
+import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
+import plotly.graph_objects as go
+import seaborn as sns
+from plotly.subplots import make_subplots
+from scipy.cluster.hierarchy import dendrogram, linkage
+from scipy.spatial.distance import squareform
+from sklearn.decomposition import PCA
+from sklearn.preprocessing import StandardScaler
+
+
+def select_top_variable_features(
+ data_dict: dict[str, pd.Series],
+ n_features: int = 500,
+ method: Literal["variance", "iqr", "cv"] = "variance",
+) -> dict[str, pd.Series]:
+ """Select the top N most variable features from a dataset.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series
+ n_features: Number of features to select
+ method: Method to measure variability ('variance', 'iqr', or 'cv')
+
+ Returns:
+ Filtered dictionary with only the most variable features
+
+ """
+ if len(data_dict) <= n_features:
+ return data_dict
+
+ # Calculate variability metric for each feature
+ variability_scores = {}
+ for var_name, values in data_dict.items():
+ clean_values = values.dropna()
+ if len(clean_values) == 0:
+ variability_scores[var_name] = 0
+ continue
+
+ if method == "variance":
+ variability_scores[var_name] = clean_values.var()
+ elif method == "iqr":
+ variability_scores[var_name] = clean_values.quantile(0.75) - clean_values.quantile(0.25)
+ elif method == "cv":
+ mean_val = clean_values.mean()
+ if abs(mean_val) > 1e-10:
+ variability_scores[var_name] = clean_values.std() / abs(mean_val)
+ else:
+ variability_scores[var_name] = 0
+
+ # Sort by variability and select top N
+ sorted_vars = sorted(variability_scores.items(), key=lambda x: x[1], reverse=True)
+ top_vars = [var_name for var_name, _ in sorted_vars[:n_features]]
+
+ return {k: v for k, v in data_dict.items() if k in top_vars}
+
+
+def create_matplotlib_correlation_heatmap(
+ data_dict: dict[str, pd.Series],
+ method: Literal["pearson", "kendall", "spearman"] = "pearson",
+ cluster: bool = False,
+ max_labels: int = 100,
+) -> plt.Figure:
+ """Create a correlation heatmap using matplotlib for large datasets.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series with cell_ids as index
+ method: Correlation method ('pearson', 'spearman', or 'kendall')
+ cluster: Whether to reorder variables by hierarchical clustering
+ max_labels: Maximum number of labels to show on axes
+
+ Returns:
+ Matplotlib Figure with correlation heatmap
+
+ """
+ if len(data_dict) < 2:
+ fig, ax = plt.subplots(figsize=(8, 6))
+ ax.text(0.5, 0.5, "Need at least 2 variables for correlation analysis", ha="center", va="center", fontsize=12)
+ ax.axis("off")
+ return fig
+
+ # Create DataFrame from all variables
+ df = pd.DataFrame(data_dict)
+
+ # Drop rows with any NaN to get valid correlations
+ df_clean = df.dropna()
+
+ if len(df_clean) == 0:
+ fig, ax = plt.subplots(figsize=(8, 6))
+ ax.text(0.5, 0.5, "No overlapping non-null data between variables", ha="center", va="center", fontsize=12)
+ ax.axis("off")
+ return fig
+
+ # Subsample if too many rows for performance
+ if len(df_clean) > 50000:
+ df_clean = df_clean.sample(n=50000, random_state=42)
+
+ # Calculate correlation matrix
+ corr = df_clean.corr(method=method)
+
+ # Apply hierarchical clustering if requested
+ if cluster and len(corr) > 2:
+ # Use correlation distance for clustering
+ corr_dist = 1 - np.abs(corr.values)
+ np.fill_diagonal(corr_dist, 0)
+ condensed_dist = squareform(corr_dist, checks=False)
+ linkage_matrix = linkage(condensed_dist, method="average")
+ dendro = dendrogram(linkage_matrix, no_plot=True)
+ cluster_order = dendro["leaves"]
+
+ # Reorder correlation matrix
+ corr = corr.iloc[cluster_order, cluster_order]
+
+ # Determine figure size based on number of variables
+ n_vars = len(corr)
+ fig_size = max(10, min(n_vars * 0.3, 50))
+
+ # Create figure
+ fig, ax = plt.subplots(figsize=(fig_size, fig_size))
+
+ # Create heatmap using seaborn
+ sns.heatmap(
+ corr,
+ cmap="RdBu_r",
+ center=0,
+ vmin=-1,
+ vmax=1,
+ square=True,
+ linewidths=0.5 if n_vars < 50 else 0,
+ cbar_kws={"shrink": 0.8, "label": f"{method.title()} Correlation"},
+ ax=ax,
+ xticklabels=n_vars <= max_labels,
+ yticklabels=n_vars <= max_labels,
+ )
+
+ # Set title
+ title = f"Correlation Matrix ({method.title()}, {n_vars} variables)"
+ if cluster:
+ title += " - Hierarchically Clustered"
+ ax.set_title(title, fontsize=14, pad=20)
+
+ # Rotate labels if shown
+ if n_vars <= max_labels:
+ plt.setp(ax.get_xticklabels(), rotation=45, ha="right", fontsize=8)
+ plt.setp(ax.get_yticklabels(), rotation=0, fontsize=8)
+
+ plt.tight_layout()
+ return fig
+
+
+def create_full_correlation_heatmap(
+ data_dict: dict[str, pd.Series],
+ method: Literal["pearson", "kendall", "spearman"] = "pearson",
+ cluster: bool = False,
+) -> go.Figure:
+ """Create a correlation heatmap for all variables across all datasets.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series with cell_ids as index
+ method: Correlation method ('pearson', 'spearman', or 'kendall')
+ cluster: Whether to reorder variables by hierarchical clustering
+
+ Returns:
+ Plotly Figure with correlation heatmap
+
+ """
+ if len(data_dict) < 2:
+ return go.Figure().add_annotation(
+ text="Need at least 2 variables for correlation analysis",
+ xref="paper",
+ yref="paper",
+ x=0.5,
+ y=0.5,
+ showarrow=False,
+ )
+
+ # Create DataFrame from all variables
+ df = pd.DataFrame(data_dict)
+
+ # Drop rows with any NaN to get valid correlations
+ df_clean = df.dropna()
+
+ if len(df_clean) == 0:
+ return go.Figure().add_annotation(
+ text="No overlapping non-null data between variables",
+ xref="paper",
+ yref="paper",
+ x=0.5,
+ y=0.5,
+ showarrow=False,
+ )
+
+ # Subsample if too many rows for performance
+ if len(df_clean) > 50000:
+ df_clean = df_clean.sample(n=50000, random_state=42)
+
+ # Calculate correlation matrix
+ corr = df_clean.corr(method=method)
+
+ # Apply hierarchical clustering if requested
+ if cluster and len(corr) > 2:
+ # Use correlation distance for clustering
+ corr_dist = 1 - np.abs(corr.values)
+ np.fill_diagonal(corr_dist, 0) # Ensure diagonal is 0
+ condensed_dist = squareform(corr_dist, checks=False)
+ linkage_matrix = linkage(condensed_dist, method="average")
+ dendro = dendrogram(linkage_matrix, no_plot=True)
+ cluster_order = dendro["leaves"]
+
+ # Reorder correlation matrix
+ corr = corr.iloc[cluster_order, cluster_order]
+
+ # Create heatmap
+ fig = go.Figure(
+ data=go.Heatmap(
+ z=corr.values,
+ x=corr.columns,
+ y=corr.index,
+ colorscale="RdBu_r",
+ zmid=0,
+ zmin=-1,
+ zmax=1,
+ text=np.round(corr.values, 2),
+ texttemplate="%{text}",
+ textfont={"size": 8},
+ colorbar={"title": f"{method.title()}
Correlation"},
+ hovertemplate=f"%{{y}} vs %{{x}}
{method.title()} correlation: %{{z:.3f}}",
+ )
+ )
+
+ title = f"Cross-Dataset Variable Correlations ({method.title()})"
+ if cluster:
+ title += " - Hierarchically Clustered"
+
+ fig.update_layout(
+ title=title,
+ height=max(600, len(corr) * 15),
+ width=max(800, len(corr) * 15),
+ xaxis={"side": "bottom", "tickangle": 45},
+ yaxis={"tickangle": 0},
+ )
+
+ return fig
+
+
+def create_pca_biplot(
+ data_dict: dict[str, pd.Series],
+ n_components: int = 2,
+ show_loadings: bool = True,
+) -> go.Figure:
+ """Create a PCA biplot showing feature relationships in principal component space.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series with cell_ids as index
+ n_components: Number of principal components to compute
+ show_loadings: Whether to show loading vectors
+
+ Returns:
+ Plotly Figure with PCA biplot
+
+ """
+ if len(data_dict) < 2:
+ return go.Figure().add_annotation(
+ text="Need at least 2 variables for PCA analysis",
+ xref="paper",
+ yref="paper",
+ x=0.5,
+ y=0.5,
+ showarrow=False,
+ )
+
+ # Create DataFrame and clean
+ df = pd.DataFrame(data_dict).dropna()
+
+ if len(df) < 10:
+ return go.Figure().add_annotation(
+ text="Not enough overlapping data for PCA analysis",
+ xref="paper",
+ yref="paper",
+ x=0.5,
+ y=0.5,
+ showarrow=False,
+ )
+
+ # Subsample if needed
+ if len(df) > 50000:
+ df = df.sample(n=50000, random_state=42)
+
+ # Standardize features
+ scaler = StandardScaler()
+ scaled_data = scaler.fit_transform(df)
+
+ # Perform PCA
+ n_components = min(n_components, len(data_dict), len(df))
+ pca = PCA(n_components=n_components)
+ pca_result = pca.fit_transform(scaled_data)
+
+ # Create figure
+ fig = go.Figure()
+
+ if n_components >= 2:
+ # Scatter plot of observations (subsampled for visibility)
+ sample_size = min(5000, len(pca_result))
+ rng = np.random.default_rng(42)
+ indices = rng.choice(len(pca_result), sample_size, replace=False)
+
+ fig.add_trace(
+ go.Scatter(
+ x=pca_result[indices, 0],
+ y=pca_result[indices, 1],
+ mode="markers",
+ marker={"size": 3, "opacity": 0.3, "color": "lightblue"},
+ name="Observations",
+ showlegend=True,
+ )
+ )
+
+ # Add loading vectors if requested
+ if show_loadings:
+ loadings = pca.components_.T * np.sqrt(pca.explained_variance_)
+
+ # Scale loadings for visibility
+ max_loading = np.abs(loadings[:, :2]).max()
+ max_data = max(np.abs(pca_result[:, :2]).max(axis=0))
+ scale = max_data / max_loading * 0.8
+
+ for i, var_name in enumerate(df.columns):
+ fig.add_trace(
+ go.Scatter(
+ x=[0, loadings[i, 0] * scale],
+ y=[0, loadings[i, 1] * scale],
+ mode="lines+text",
+ line={"color": "red", "width": 2},
+ text=["", var_name],
+ textposition="top center",
+ textfont={"size": 10, "color": "darkred"},
+ name=var_name,
+ showlegend=False,
+ hovertemplate=f"{var_name}
"
+ + f"PC1 loading: {loadings[i, 0]:.3f}
"
+ + f"PC2 loading: {loadings[i, 1]:.3f}",
+ )
+ )
+
+ var_exp = pca.explained_variance_ratio_
+ fig.update_xaxes(title=f"PC1 ({var_exp[0] * 100:.1f}% variance)")
+ fig.update_yaxes(title=f"PC2 ({var_exp[1] * 100:.1f}% variance)")
+
+ fig.update_layout(
+ title="PCA Biplot - Feature Relationships in Principal Component Space",
+ height=700,
+ showlegend=show_loadings,
+ hovermode="closest",
+ )
+
+ return fig
+
+
+def create_dendrogram_plot(
+ data_dict: dict[str, pd.Series],
+ method: Literal["pearson", "kendall", "spearman"] = "pearson",
+) -> go.Figure:
+ """Create a dendrogram showing hierarchical clustering of variables based on correlation.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series with cell_ids as index
+ method: Correlation method to use for distance calculation
+
+ Returns:
+ Plotly Figure with dendrogram
+
+ """
+ if len(data_dict) < 2:
+ return go.Figure().add_annotation(
+ text="Need at least 2 variables for clustering",
+ xref="paper",
+ yref="paper",
+ x=0.5,
+ y=0.5,
+ showarrow=False,
+ )
+
+ # Create DataFrame and clean
+ df = pd.DataFrame(data_dict).dropna()
+
+ if len(df) < 10:
+ return go.Figure().add_annotation(
+ text="Not enough overlapping data for clustering",
+ xref="paper",
+ yref="paper",
+ x=0.5,
+ y=0.5,
+ showarrow=False,
+ )
+
+ # Subsample if needed
+ if len(df) > 50000:
+ df = df.sample(n=50000, random_state=42)
+
+ # Calculate correlation and convert to distance
+ corr = df.corr(method=method)
+ corr_dist = 1 - np.abs(corr.values)
+ np.fill_diagonal(corr_dist, 0)
+
+ # Perform hierarchical clustering
+ condensed_dist = squareform(corr_dist, checks=False)
+ linkage_matrix = linkage(condensed_dist, method="average")
+
+ # Create dendrogram
+ dendro = dendrogram(linkage_matrix, labels=list(df.columns), no_plot=True)
+
+ # Extract dendrogram data
+ icoord = np.array(dendro["icoord"])
+ dcoord = np.array(dendro["dcoord"])
+
+ # Create plotly figure
+ fig = go.Figure()
+
+ # Add dendrogram lines
+ for i in range(len(icoord)):
+ fig.add_trace(
+ go.Scatter(
+ x=icoord[i],
+ y=dcoord[i],
+ mode="lines",
+ line={"color": "black", "width": 1.5},
+ hoverinfo="skip",
+ showlegend=False,
+ )
+ )
+
+ # Add labels
+ labels = dendro["ivl"]
+ label_pos = np.arange(5, len(labels) * 10 + 5, 10)
+
+ fig.update_layout(
+ title=f"Hierarchical Clustering of Variables (based on {method.title()} correlation)",
+ xaxis={
+ "title": "Variables",
+ "tickmode": "array",
+ "tickvals": label_pos,
+ "ticktext": labels,
+ "tickangle": 45,
+ },
+ yaxis={"title": "Distance (1 - |correlation|)"},
+ height=600,
+ showlegend=False,
+ )
+
+ return fig
+
+
+def create_mutual_information_matrix(data_dict: dict[str, pd.Series]) -> go.Figure:
+ """Create a mutual information matrix to capture non-linear relationships.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series with cell_ids as index
+
+ Returns:
+ Plotly Figure with mutual information heatmap
+
+ """
+ from sklearn.feature_selection import mutual_info_regression
+
+ if len(data_dict) < 2:
+ return go.Figure().add_annotation(
+ text="Need at least 2 variables for mutual information analysis",
+ xref="paper",
+ yref="paper",
+ x=0.5,
+ y=0.5,
+ showarrow=False,
+ )
+
+ # Create DataFrame and clean
+ df = pd.DataFrame(data_dict).dropna()
+
+ if len(df) < 100:
+ return go.Figure().add_annotation(
+ text="Not enough overlapping data for mutual information analysis",
+ xref="paper",
+ yref="paper",
+ x=0.5,
+ y=0.5,
+ showarrow=False,
+ )
+
+ # Subsample if needed
+ if len(df) > 10000: # MI is computationally expensive
+ df = df.sample(n=10000, random_state=42)
+
+ # Calculate pairwise mutual information
+ n_vars = len(df.columns)
+ mi_matrix = np.zeros((n_vars, n_vars))
+
+ for i, col_i in enumerate(df.columns):
+ for j, col_j in enumerate(df.columns):
+ if i == j:
+ mi_matrix[i, j] = 1.0 # Self-information (normalized)
+ elif i < j:
+ # Calculate MI
+ X = df[col_i].to_numpy().reshape(-1, 1) # noqa: N806
+ y = df[col_j].to_numpy()
+ mi = mutual_info_regression(X, y, random_state=42)[0]
+
+ # Normalize by entropy (approximate)
+ mi_norm = mi / (np.std(y) * np.std(X.flatten()) + 1e-10)
+ mi_matrix[i, j] = mi_norm
+ mi_matrix[j, i] = mi_norm
+
+ # Create heatmap
+ fig = go.Figure(
+ data=go.Heatmap(
+ z=mi_matrix,
+ x=list(df.columns),
+ y=list(df.columns),
+ colorscale="YlOrRd",
+ zmin=0,
+ text=np.round(mi_matrix, 3),
+ texttemplate="%{text}",
+ textfont={"size": 8},
+ colorbar={"title": "Normalized
Mutual Info"},
+ hovertemplate="%{y} vs %{x}
Mutual Information: %{z:.3f}",
+ )
+ )
+
+ fig.update_layout(
+ title="Mutual Information Matrix (captures non-linear relationships)",
+ height=max(600, n_vars * 15),
+ width=max(800, n_vars * 15),
+ xaxis={"side": "bottom", "tickangle": 45},
+ yaxis={"tickangle": 0},
+ )
+
+ return fig
+
+
+def create_feature_variance_plot(data_dict: dict[str, pd.Series]) -> go.Figure:
+ """Create a bar plot showing variance/spread of each feature.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series with cell_ids as index
+
+ Returns:
+ Plotly Figure with variance bar plot
+
+ """
+ if len(data_dict) == 0:
+ return go.Figure().add_annotation(
+ text="No variables provided",
+ xref="paper",
+ yref="paper",
+ x=0.5,
+ y=0.5,
+ showarrow=False,
+ )
+
+ # Calculate statistics for each variable
+ stats_data = []
+ for var_name, values in data_dict.items():
+ clean_values = values.dropna()
+ if len(clean_values) > 0:
+ stats_data.append(
+ {
+ "Variable": var_name,
+ "Std Dev": clean_values.std(),
+ "Variance": clean_values.var(),
+ "IQR": clean_values.quantile(0.75) - clean_values.quantile(0.25),
+ "Range": clean_values.max() - clean_values.min(),
+ "CV": clean_values.std() / (abs(clean_values.mean()) + 1e-10),
+ }
+ )
+
+ stats_df = pd.DataFrame(stats_data).sort_values("Variance", ascending=False)
+
+ # Create subplots for different variance metrics
+ fig = make_subplots(
+ rows=2,
+ cols=2,
+ subplot_titles=["Standard Deviation", "Interquartile Range (IQR)", "Range", "Coefficient of Variation"],
+ vertical_spacing=0.12,
+ horizontal_spacing=0.10,
+ )
+
+ metrics = [
+ ("Std Dev", 1, 1),
+ ("IQR", 1, 2),
+ ("Range", 2, 1),
+ ("CV", 2, 2),
+ ]
+
+ for metric, row, col in metrics:
+ fig.add_trace(
+ go.Bar(
+ x=stats_df["Variable"],
+ y=stats_df[metric],
+ name=metric,
+ marker={"color": stats_df[metric], "colorscale": "Viridis"},
+ showlegend=False,
+ hovertemplate=f"%{{x}}
{metric}: %{{y:.3f}}",
+ ),
+ row=row,
+ col=col,
+ )
+
+ fig.update_xaxes(tickangle=45, row=row, col=col)
+ fig.update_yaxes(title_text=metric, row=row, col=col)
+
+ fig.update_layout(
+ title="Feature Variance and Spread Metrics",
+ height=800,
+ showlegend=False,
+ )
+
+ return fig
diff --git a/src/entropice/dashboard/plots/experiment_comparison.py b/src/entropice/dashboard/plots/experiment_comparison.py
new file mode 100644
index 0000000..a2577e7
--- /dev/null
+++ b/src/entropice/dashboard/plots/experiment_comparison.py
@@ -0,0 +1,972 @@
+"""Plots for experiment comparison and analysis."""
+
+from typing import TYPE_CHECKING
+
+import pandas as pd
+import plotly.express as px
+import plotly.graph_objects as go
+
+from entropice.dashboard.utils.colors import get_palette
+from entropice.dashboard.utils.formatters import format_metric_name
+
+if TYPE_CHECKING:
+ import geopandas as gpd
+ import pydeck as pdk
+
+
+def create_grid_level_comparison_plot(
+ results_df: pd.DataFrame,
+ metric: str,
+ split: str = "test",
+) -> go.Figure:
+ """Create a plot comparing model performance across grid levels.
+
+ Args:
+ results_df: DataFrame with experiment results including grid, level, model, and metrics
+ metric: Metric to compare (e.g., 'f1', 'accuracy', 'r2')
+ split: Data split to show ('train', 'test', or 'combined')
+
+ Returns:
+ Plotly figure showing performance across grid levels
+
+ """
+ metric_col = f"{split}_{metric}"
+
+ if metric_col not in results_df.columns:
+ raise ValueError(f"Metric {metric_col} not found in results")
+
+ # Create grid_level column for grouping
+ results_df = results_df.copy()
+ results_df["grid_level"] = results_df["grid"] + "_" + results_df["level"].astype(str)
+
+ # Define the proper order for grid levels by resolution
+ grid_level_order = [
+ "hex_3",
+ "healpix_6",
+ "healpix_7",
+ "hex_4",
+ "healpix_8",
+ "hex_5",
+ "healpix_9",
+ "healpix_10",
+ "hex_6",
+ ]
+
+ # Create display labels for grid levels
+ grid_level_display = {
+ "hex_3": "Hex-3",
+ "healpix_6": "Healpix-6",
+ "healpix_7": "Healpix-7",
+ "hex_4": "Hex-4",
+ "healpix_8": "Healpix-8",
+ "hex_5": "Hex-5",
+ "healpix_9": "Healpix-9",
+ "healpix_10": "Healpix-10",
+ "hex_6": "Hex-6",
+ }
+
+ # Add display column
+ results_df["grid_level_display"] = results_df["grid_level"].map(grid_level_display)
+
+ # Create color map for target datasets (use 2nd color from 5-color palette for more saturation)
+ unique_targets = results_df["target"].unique()
+ target_colors = {target: get_palette(target, 5)[1] for target in unique_targets}
+
+ # Create symbol map for models
+ model_symbols = {
+ "espa": "circle",
+ "xgboost": "square",
+ "rf": "diamond",
+ "knn": "cross",
+ "ensemble": "star",
+ "autogluon": "star",
+ }
+
+ # Add a combined column for hover information
+ results_df["model_display"] = results_df["model"].str.upper()
+
+ # Create box plot without individual points first
+ fig = px.box(
+ results_df,
+ x="grid_level_display",
+ y=metric_col,
+ color="target",
+ facet_col="task",
+ points=False, # We'll add points separately with symbols
+ title=f"{format_metric_name(metric)} by Grid Level ({split.capitalize()} Set)",
+ labels={
+ metric_col: format_metric_name(metric),
+ "grid_level_display": "Grid Level",
+ "target": "Target Dataset",
+ },
+ color_discrete_map=target_colors,
+ category_orders={"grid_level_display": [grid_level_display[gl] for gl in grid_level_order]},
+ )
+
+ # Add scatter points with model-specific symbols
+ # Group by task for faceting
+ if "task" in results_df.columns:
+ unique_tasks = results_df["task"].unique()
+ else:
+ unique_tasks = [None]
+
+ # Add scatter traces and line traces for each target-model combination
+ for target in unique_targets:
+ target_data = results_df[results_df["target"] == target]
+ for model in target_data["model"].unique():
+ model_data = target_data[target_data["model"] == model]
+ symbol = model_symbols.get(model, "circle")
+
+ # Add scatter trace for each task facet
+ for task_idx, task in enumerate(unique_tasks):
+ if task is not None:
+ task_data = model_data[model_data["task"] == task]
+ else:
+ task_data = model_data
+
+ if len(task_data) == 0:
+ continue
+
+ # Sort by grid_level order for proper line connections
+ task_data["grid_level_order"] = task_data["grid_level"].map(
+ {gl: i for i, gl in enumerate(grid_level_order)}
+ )
+ task_data = task_data.sort_values("grid_level_order")
+
+ # Determine which subplot this goes to
+ row = 1
+ col = task_idx + 1 if task is not None else 1
+
+ # Add line trace connecting points of the same model
+ line = go.Scatter(
+ x=task_data["grid_level_display"],
+ y=task_data[metric_col],
+ mode="lines",
+ line={
+ "color": target_colors[target],
+ "width": 1.5,
+ "dash": "dot",
+ },
+ showlegend=False,
+ hoverinfo="skip",
+ xaxis=f"x{col}" if col > 1 else "x",
+ yaxis=f"y{col}" if col > 1 else "y",
+ )
+
+ fig.add_trace(line, row=row, col=col)
+
+ # Add scatter trace on top of the line
+ scatter = go.Scatter(
+ x=task_data["grid_level_display"],
+ y=task_data[metric_col],
+ mode="markers",
+ marker={
+ "symbol": symbol,
+ "size": 8,
+ "color": target_colors[target],
+ "line": {"width": 1, "color": "white"},
+ },
+ name=f"{target} ({model.upper()})",
+ legendgroup=target,
+ showlegend=(task_idx == 0), # Only show in legend once
+ hovertemplate=(
+ f"{target}
"
+ f"Model: {model.upper()}
"
+ f"Grid Level: %{{x}}
"
+ f"{format_metric_name(metric)}: %{{y:.4f}}
"
+ ""
+ ),
+ xaxis=f"x{col}" if col > 1 else "x",
+ yaxis=f"y{col}" if col > 1 else "y",
+ )
+
+ fig.add_trace(scatter, row=row, col=col)
+
+ fig.update_layout(
+ height=500,
+ showlegend=True,
+ hovermode="closest",
+ )
+
+ return fig
+
+
+def create_model_ranking_plot(
+ results_df: pd.DataFrame,
+ metric: str,
+ split: str = "test",
+ top_n: int = 10,
+) -> go.Figure:
+ """Create a plot ranking models by performance.
+
+ Args:
+ results_df: DataFrame with experiment results
+ metric: Metric to rank by
+ split: Data split to show
+ top_n: Number of top models to show
+
+ Returns:
+ Plotly figure showing top models
+
+ """
+ metric_col = f"{split}_{metric}"
+
+ if metric_col not in results_df.columns:
+ raise ValueError(f"Metric {metric_col} not found in results")
+
+ # Get top N models
+ top_models = results_df.nlargest(top_n, metric_col)
+
+ # Create display label
+ top_models = top_models.copy()
+ top_models["model_label"] = (
+ top_models["model"].astype(str)
+ + " ("
+ + top_models["grid"]
+ + "_"
+ + top_models["level"].astype(str)
+ + ", "
+ + top_models["task"]
+ + ")"
+ )
+
+ colors = get_palette("models", len(top_models))
+
+ fig = go.Figure(
+ data=[
+ go.Bar(
+ x=top_models[metric_col],
+ y=top_models["model_label"],
+ orientation="h",
+ marker={"color": colors},
+ text=top_models[metric_col].round(4),
+ textposition="auto",
+ )
+ ]
+ )
+
+ fig.update_layout(
+ title=f"Top {top_n} Models by {format_metric_name(metric)} ({split.capitalize()} Set)",
+ xaxis_title=format_metric_name(metric),
+ yaxis_title="Model Configuration",
+ height=max(400, top_n * 40),
+ showlegend=False,
+ )
+
+ return fig
+
+
+def create_model_consistency_heatmap(
+ results_df: pd.DataFrame,
+ metric: str,
+ split: str = "test",
+) -> go.Figure:
+ """Create a heatmap showing model consistency across tasks and targets.
+
+ Args:
+ results_df: DataFrame with experiment results
+ metric: Metric to analyze
+ split: Data split to show
+
+ Returns:
+ Plotly figure showing heatmap of model performance
+
+ """
+ metric_col = f"{split}_{metric}"
+
+ if metric_col not in results_df.columns:
+ raise ValueError(f"Metric {metric_col} not found in results")
+
+ # Create a pivot table: models vs (task, target)
+ results_df = results_df.copy()
+ results_df["task_target"] = results_df["task"] + "_" + results_df["target"]
+
+ # Get best score per model-task-target combination
+ pivot_data = results_df.groupby(["model", "task_target"])[metric_col].max().reset_index()
+ pivot_table = pivot_data.pivot_table(index="model", columns="task_target", values=metric_col)
+
+ fig = go.Figure(
+ data=go.Heatmap(
+ z=pivot_table.to_numpy(),
+ x=pivot_table.columns,
+ y=pivot_table.index,
+ colorscale="Viridis",
+ text=pivot_table.to_numpy().round(3),
+ texttemplate="%{text}",
+ textfont={"size": 10},
+ colorbar={"title": format_metric_name(metric)},
+ )
+ )
+
+ fig.update_layout(
+ title=f"Model Consistency: {format_metric_name(metric)} Across Tasks and Targets ({split.capitalize()} Set)",
+ xaxis_title="Task_Target",
+ yaxis_title="Model",
+ height=max(400, len(pivot_table.index) * 50),
+ )
+
+ return fig
+
+
+def create_feature_importance_comparison_plot(
+ feature_importance_df: pd.DataFrame,
+ top_n: int = 20,
+) -> go.Figure:
+ """Create a plot comparing feature importance across different models/configurations.
+
+ Args:
+ feature_importance_df: DataFrame with columns: feature, importance, model, grid, level, task
+ top_n: Number of top features to show
+
+ Returns:
+ Plotly figure showing feature importance comparison
+
+ """
+ # Get top features overall
+ overall_importance = feature_importance_df.groupby("feature")["importance"].mean().reset_index()
+ top_features = overall_importance.nlargest(top_n, "importance")["feature"].tolist()
+
+ # Filter to top features
+ filtered_df = feature_importance_df[feature_importance_df["feature"].isin(top_features)]
+
+ # Create grid_level column
+ filtered_df = filtered_df.copy()
+ filtered_df["config"] = (
+ filtered_df["model"].astype(str)
+ + " ("
+ + filtered_df["grid"]
+ + "_"
+ + filtered_df["level"].astype(str)
+ + ", "
+ + filtered_df["task"]
+ + ")"
+ )
+
+ # Create grouped bar chart
+ fig = px.bar(
+ filtered_df,
+ x="feature",
+ y="importance",
+ color="config",
+ barmode="group",
+ title=f"Top {top_n} Features: Importance Comparison Across Configurations",
+ labels={"importance": "Feature Importance", "feature": "Feature", "config": "Configuration"},
+ )
+
+ fig.update_layout(
+ height=600,
+ xaxis_tickangle=-45,
+ showlegend=True,
+ legend={"orientation": "v", "yanchor": "top", "y": 1, "xanchor": "left", "x": 1.02},
+ )
+
+ return fig
+
+
+def create_feature_importance_heatmap(
+ feature_importance_df: pd.DataFrame,
+ top_n: int = 30,
+) -> go.Figure:
+ """Create a heatmap of feature importance across configurations.
+
+ Args:
+ feature_importance_df: DataFrame with columns: feature, importance, model, grid, level, task
+ top_n: Number of top features to show
+
+ Returns:
+ Plotly figure showing heatmap
+
+ """
+ # Get top features overall
+ overall_importance = feature_importance_df.groupby("feature")["importance"].mean().reset_index()
+ top_features = overall_importance.nlargest(top_n, "importance")["feature"].tolist()
+
+ # Filter to top features
+ filtered_df = feature_importance_df[feature_importance_df["feature"].isin(top_features)]
+
+ # Create config column
+ filtered_df = filtered_df.copy()
+ filtered_df["config"] = (
+ filtered_df["grid"]
+ + "_"
+ + filtered_df["level"].astype(str)
+ + "_"
+ + filtered_df["task"]
+ + "_"
+ + filtered_df["model"].astype(str)
+ )
+
+ # Pivot: features vs configs
+ pivot_table = filtered_df.pivot_table(
+ index="feature", columns="config", values="importance", aggfunc="mean", fill_value=0
+ )
+
+ fig = go.Figure(
+ data=go.Heatmap(
+ z=pivot_table.to_numpy(),
+ x=pivot_table.columns,
+ y=pivot_table.index,
+ colorscale="Viridis",
+ colorbar={"title": "Importance"},
+ )
+ )
+
+ fig.update_layout(
+ title=f"Top {top_n} Features: Importance Heatmap Across Configurations",
+ xaxis_title="Configuration",
+ yaxis_title="Feature",
+ height=max(500, len(pivot_table.index) * 20),
+ xaxis_tickangle=-45,
+ )
+
+ return fig
+
+
+def create_model_performance_distribution(
+ results_df: pd.DataFrame,
+ metric: str,
+ split: str = "test",
+) -> go.Figure:
+ """Create violin plots showing performance distribution by model type.
+
+ Args:
+ results_df: DataFrame with experiment results
+ metric: Metric to analyze
+ split: Data split to show
+
+ Returns:
+ Plotly figure with violin plots
+
+ """
+ metric_col = f"{split}_{metric}"
+
+ if metric_col not in results_df.columns:
+ raise ValueError(f"Metric {metric_col} not found in results")
+
+ colors = get_palette("models", results_df["model"].nunique())
+ color_map = {model: colors[i] for i, model in enumerate(results_df["model"].unique())}
+
+ fig = px.violin(
+ results_df,
+ x="model",
+ y=metric_col,
+ color="model",
+ box=True,
+ points="all",
+ title=f"{format_metric_name(metric)} Distribution by Model Type ({split.capitalize()} Set)",
+ labels={metric_col: format_metric_name(metric), "model": "Model"},
+ color_discrete_map=color_map,
+ )
+
+ fig.update_layout(
+ height=500,
+ showlegend=False,
+ )
+
+ return fig
+
+
+def create_grid_vs_model_performance(
+ results_df: pd.DataFrame,
+ metric: str,
+ split: str = "test",
+) -> go.Figure:
+ """Create a faceted plot showing model performance across grids.
+
+ Args:
+ results_df: DataFrame with experiment results
+ metric: Metric to analyze
+ split: Data split to show
+
+ Returns:
+ Plotly figure with faceted plots
+
+ """
+ metric_col = f"{split}_{metric}"
+
+ if metric_col not in results_df.columns:
+ raise ValueError(f"Metric {metric_col} not found in results")
+
+ results_df = results_df.copy()
+ results_df["grid_level"] = results_df["grid"] + "_" + results_df["level"].astype(str)
+
+ fig = px.box(
+ results_df,
+ x="model",
+ y=metric_col,
+ color="model",
+ facet_col="grid_level",
+ facet_row="task",
+ points="all",
+ title=f"{format_metric_name(metric)}: Model Performance Across Grid Levels and Tasks ({split.capitalize()})",
+ labels={metric_col: format_metric_name(metric), "model": "Model"},
+ )
+
+ fig.update_layout(
+ height=800,
+ showlegend=True,
+ )
+
+ return fig
+
+
+def create_performance_improvement_plot(
+ results_df: pd.DataFrame,
+ metric: str,
+ baseline_model: str = "rf",
+ split: str = "test",
+) -> go.Figure:
+ """Create a plot showing performance improvement relative to a baseline model.
+
+ Args:
+ results_df: DataFrame with experiment results
+ metric: Metric to analyze
+ baseline_model: Model to use as baseline for comparison
+ split: Data split to show
+
+ Returns:
+ Plotly figure showing improvement over baseline
+
+ """
+ metric_col = f"{split}_{metric}"
+
+ if metric_col not in results_df.columns:
+ raise ValueError(f"Metric {metric_col} not found in results")
+
+ results_df = results_df.copy()
+
+ # Calculate baseline performance for each task-target-grid-level combination
+ baseline_perf = (
+ results_df[results_df["model"] == baseline_model]
+ .groupby(["task", "target", "grid", "level"])[metric_col]
+ .max()
+ .reset_index()
+ .rename(columns={metric_col: "baseline_score"})
+ )
+
+ # Merge with all results
+ results_with_baseline = results_df.merge(baseline_perf, on=["task", "target", "grid", "level"], how="left")
+
+ # Calculate improvement
+ results_with_baseline["improvement"] = results_with_baseline[metric_col] - results_with_baseline["baseline_score"]
+ results_with_baseline["improvement_pct"] = (
+ 100 * results_with_baseline["improvement"] / results_with_baseline["baseline_score"]
+ )
+
+ # Filter out baseline model itself
+ results_with_baseline = results_with_baseline[results_with_baseline["model"] != baseline_model]
+
+ # Create plot
+ fig = px.box(
+ results_with_baseline,
+ x="model",
+ y="improvement_pct",
+ color="model",
+ points="all",
+ title=(
+ f"Performance Improvement Over {baseline_model.upper()} Baseline "
+ f"({format_metric_name(metric)}, {split.capitalize()} Set)"
+ ),
+ labels={"improvement_pct": "Improvement (%)", "model": "Model"},
+ )
+
+ fig.add_hline(y=0, line_dash="dash", line_color="gray", annotation_text="Baseline")
+
+ fig.update_layout(
+ height=500,
+ showlegend=False,
+ )
+
+ return fig
+
+
+def create_top_models_bar_chart(
+ task_df: pd.DataFrame,
+ metric: str,
+ task_name: str,
+ split: str = "test",
+ top_n: int = 10,
+) -> go.Figure:
+ """Create a horizontal bar chart showing top models for a task.
+
+ Args:
+ task_df: DataFrame filtered for a specific task
+ metric: Metric to display
+ task_name: Name of the task for the title
+ split: Data split to show
+ top_n: Number of top models to show
+
+ Returns:
+ Plotly figure with horizontal bar chart
+
+ """
+ metric_col = f"{split}_{metric}"
+
+ if metric_col not in task_df.columns:
+ raise ValueError(f"Metric {metric_col} not found in data")
+
+ # Get top N models
+ top_models = task_df.nlargest(top_n, metric_col).copy()
+
+ # Create label combining model, grid level, and target
+ top_models["label"] = (
+ top_models["model"].str.upper()
+ + " ("
+ + top_models["grid"]
+ + "-"
+ + top_models["level"].astype(str)
+ + ", "
+ + top_models["target"]
+ + ")"
+ )
+
+ # Sort by score for display (ascending so best is on top)
+ top_models = top_models.sort_values(metric_col, ascending=True)
+
+ # Create color map based on model type
+ unique_models = top_models["model"].unique()
+ model_colors = {model: get_palette("models", len(unique_models))[i] for i, model in enumerate(unique_models)}
+ top_models["color"] = top_models["model"].map(model_colors)
+
+ fig = go.Figure(
+ data=[
+ go.Bar(
+ y=top_models["label"],
+ x=top_models[metric_col],
+ orientation="h",
+ marker={"color": top_models["color"]},
+ text=top_models[metric_col].round(4),
+ textposition="auto",
+ hovertemplate=("%{y}
" + f"{format_metric_name(metric)}: %{{x:.4f}}
" + ""),
+ )
+ ]
+ )
+
+ fig.update_layout(
+ title=f"Top {top_n} Models - {task_name.replace('_', ' ').title()}",
+ xaxis_title=format_metric_name(metric),
+ yaxis_title="",
+ height=max(400, top_n * 40),
+ showlegend=False,
+ margin={"l": 250},
+ )
+
+ return fig
+
+
+def create_feature_importance_by_grid_level(
+ fi_df: pd.DataFrame,
+ top_n: int = 15,
+) -> go.Figure:
+ """Create a grouped bar chart showing top features by grid level.
+
+ Args:
+ fi_df: Feature importance DataFrame with columns: feature, importance, grid_level
+ top_n: Number of top features to show
+
+ Returns:
+ Plotly figure with grouped bar chart
+
+ """
+ # Get overall top features
+ overall_top = fi_df.groupby("feature")["importance"].mean().nlargest(top_n).index.tolist()
+
+ # Filter to top features
+ filtered = fi_df[fi_df["feature"].isin(overall_top)]
+
+ # Calculate mean importance per feature per grid level
+ grouped = filtered.groupby(["grid_level", "feature"])["importance"].mean().reset_index()
+
+ # Create the plot
+ fig = px.bar(
+ grouped,
+ x="feature",
+ y="importance",
+ color="grid_level",
+ barmode="group",
+ title=f"Top {top_n} Features by Grid Level",
+ labels={"importance": "Mean Importance", "feature": "Feature", "grid_level": "Grid Level"},
+ )
+
+ fig.update_layout(
+ height=500,
+ xaxis_tickangle=-45,
+ showlegend=True,
+ legend={"orientation": "v", "yanchor": "top", "y": 1, "xanchor": "left", "x": 1.02},
+ )
+
+ return fig
+
+
+def create_feature_consistency_plot(
+ fi_df: pd.DataFrame,
+ top_n: int = 15,
+) -> go.Figure:
+ """Create a scatter plot showing feature importance vs consistency.
+
+ Args:
+ fi_df: Feature importance DataFrame
+ top_n: Number of top features to show
+
+ Returns:
+ Plotly figure with scatter plot
+
+ """
+ # Get top features
+ overall_top = fi_df.groupby("feature")["importance"].mean().nlargest(top_n).index.tolist()
+
+ # Calculate statistics
+ stats = (
+ fi_df[fi_df["feature"].isin(overall_top)].groupby("feature")["importance"].agg(["mean", "std"]).reset_index()
+ )
+ stats["cv"] = stats["std"] / stats["mean"]
+
+ # Add data source if available
+ if "data_source" in fi_df.columns:
+ data_source_map = fi_df[["feature", "data_source"]].drop_duplicates().set_index("feature")["data_source"]
+ stats["data_source"] = stats["feature"].map(data_source_map)
+ color_col = "data_source"
+ else:
+ color_col = None
+
+ fig = px.scatter(
+ stats,
+ x="cv",
+ y="mean",
+ size="std",
+ color=color_col,
+ hover_data=["feature"],
+ text="feature",
+ title="Feature Importance vs Consistency (Coefficient of Variation)",
+ labels={
+ "cv": "Coefficient of Variation (lower = more consistent)",
+ "mean": "Mean Importance",
+ "std": "Std Dev",
+ },
+ )
+
+ fig.update_traces(textposition="top center", textfont_size=8)
+
+ fig.update_layout(
+ height=600,
+ showlegend=True,
+ )
+
+ return fig
+
+
+def create_data_source_importance_bars(
+ fi_df: pd.DataFrame,
+) -> go.Figure:
+ """Create a bar chart showing importance breakdown by data source.
+
+ Args:
+ fi_df: Feature importance DataFrame with data_source column
+
+ Returns:
+ Plotly figure with bar chart
+
+ """
+ if "data_source" not in fi_df.columns:
+ raise ValueError("data_source column not found in feature importance data")
+
+ # Aggregate by data source
+ source_stats = (
+ fi_df.groupby("data_source")["importance"]
+ .agg(["sum", "mean", "count"])
+ .reset_index()
+ .sort_values("sum", ascending=False)
+ )
+
+ # Create colors for data sources
+ colors = get_palette("sources", len(source_stats))
+
+ fig = go.Figure(
+ data=[
+ go.Bar(
+ x=source_stats["data_source"],
+ y=source_stats["sum"],
+ marker={"color": colors},
+ text=source_stats["sum"].round(2),
+ textposition="auto",
+ hovertemplate=(
+ "%{x}
"
+ "Total Importance: %{y:.2f}
"
+ "Mean: %{customdata[0]:.4f}
"
+ "Features: %{customdata[1]}
"
+ ""
+ ),
+ customdata=source_stats[["mean", "count"]].values,
+ )
+ ]
+ )
+
+ fig.update_layout(
+ title="Feature Importance by Data Source",
+ xaxis_title="Data Source",
+ yaxis_title="Total Importance",
+ height=400,
+ showlegend=False,
+ )
+
+ return fig
+
+
+def create_inference_maps(
+ inference_gdf: "gpd.GeoDataFrame",
+ grid: str,
+ level: int,
+ task: str,
+) -> tuple["pdk.Deck", "pdk.Deck"]:
+ """Create inference maps showing mean and std of predictions using pydeck.
+
+ Args:
+ inference_gdf: GeoDataFrame with geometry, mean_prediction, std_prediction columns
+ grid: Grid type (e.g., 'hex', 'healpix')
+ level: Grid resolution level
+ task: Task type for title
+
+ Returns:
+ Tuple of (mean_deck, std_deck) pydeck visualizations
+
+ """
+ import matplotlib.colors as mcolors
+ import numpy as np
+ import pydeck as pdk
+ from matplotlib.colors import LinearSegmentedColormap
+
+ from entropice.dashboard.utils.colors import hex_to_rgb
+ from entropice.dashboard.utils.geometry import fix_hex_geometry
+
+ # Create a copy and convert to EPSG:4326 for pydeck
+ gdf = inference_gdf.copy().to_crs("EPSG:4326")
+
+ # Fix antimeridian issues for hex cells
+ gdf["geometry"] = gdf["geometry"].apply(fix_hex_geometry)
+
+ # Create custom colormap for predictions (white -> blue -> purple)
+ colors_mean = ["#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", "#6baed6", "#4292c6", "#2171b5", "#08519c", "#08306b"]
+ cmap_mean = LinearSegmentedColormap.from_list("prediction", colors_mean)
+
+ colors_std = ["#f7fcf5", "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#006d2c", "#00441b"]
+ cmap_std = LinearSegmentedColormap.from_list("uncertainty", colors_std)
+
+ # Normalize mean predictions
+ mean_values = gdf["mean_prediction"].to_numpy()
+ mean_min, mean_max = np.nanpercentile(mean_values, [2, 98])
+ if mean_max > mean_min:
+ mean_normalized = np.clip((mean_values - mean_min) / (mean_max - mean_min), 0, 1)
+ else:
+ mean_normalized = np.zeros_like(mean_values)
+
+ # Map normalized values to colors for mean
+ mean_colors = [cmap_mean(val) for val in mean_normalized]
+ mean_rgb_colors = [hex_to_rgb(mcolors.to_hex(color)) for color in mean_colors]
+ gdf["mean_color"] = mean_rgb_colors
+
+ # Normalize std predictions
+ std_values = gdf["std_prediction"].to_numpy()
+ std_min, std_max = np.nanpercentile(std_values, [2, 98])
+ if std_max > std_min:
+ std_normalized = np.clip((std_values - std_min) / (std_max - std_min), 0, 1)
+ else:
+ std_normalized = np.zeros_like(std_values)
+
+ # Map normalized values to colors for std
+ std_colors = [cmap_std(val) for val in std_normalized]
+ std_rgb_colors = [hex_to_rgb(mcolors.to_hex(color)) for color in std_colors]
+ gdf["std_color"] = std_rgb_colors
+
+ # Convert to GeoJSON for mean predictions
+ geojson_mean = []
+ for _, row in gdf.iterrows():
+ feature = {
+ "type": "Feature",
+ "geometry": row["geometry"].__geo_interface__,
+ "properties": {
+ "fill_color": row["mean_color"],
+ "mean_prediction": float(row["mean_prediction"]),
+ },
+ }
+ geojson_mean.append(feature)
+
+ # Convert to GeoJSON for std predictions
+ geojson_std = []
+ for _, row in gdf.iterrows():
+ feature = {
+ "type": "Feature",
+ "geometry": row["geometry"].__geo_interface__,
+ "properties": {
+ "fill_color": row["std_color"],
+ "std_prediction": float(row["std_prediction"]),
+ },
+ }
+ geojson_std.append(feature)
+
+ # Create pydeck layer for mean predictions
+ layer_mean = pdk.Layer(
+ "GeoJsonLayer",
+ geojson_mean,
+ opacity=0.85,
+ stroked=True,
+ filled=True,
+ extruded=False,
+ wireframe=False,
+ get_fill_color="properties.fill_color",
+ get_line_color=[100, 100, 100],
+ line_width_min_pixels=0.3,
+ pickable=True,
+ )
+
+ # Create pydeck layer for std predictions
+ layer_std = pdk.Layer(
+ "GeoJsonLayer",
+ geojson_std,
+ opacity=0.85,
+ stroked=True,
+ filled=True,
+ extruded=False,
+ wireframe=False,
+ get_fill_color="properties.fill_color",
+ get_line_color=[100, 100, 100],
+ line_width_min_pixels=0.3,
+ pickable=True,
+ )
+
+ # Set initial view state for Arctic region
+ view_state = pdk.ViewState(
+ latitude=75,
+ longitude=0,
+ zoom=2.5,
+ pitch=0,
+ )
+
+ # Create deck for mean predictions
+ deck_mean = pdk.Deck(
+ layers=[layer_mean],
+ initial_view_state=view_state,
+ tooltip={
+ "html": (
+ f"Mean Prediction: {{mean_prediction}}
"
+ f"{grid.upper()}-{level} | Task: {task.title()}"
+ ),
+ "style": {"backgroundColor": "#2171b5", "color": "white"},
+ },
+ map_style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
+ )
+
+ # Create deck for std predictions
+ deck_std = pdk.Deck(
+ layers=[layer_std],
+ initial_view_state=view_state,
+ tooltip={
+ "html": (
+ f"Uncertainty (Std): {{std_prediction}}
"
+ f"{grid.upper()}-{level} | Task: {task.title()}"
+ ),
+ "style": {"backgroundColor": "#238b45", "color": "white"},
+ },
+ map_style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
+ )
+
+ return deck_mean, deck_std
diff --git a/src/entropice/dashboard/plots/inference.py b/src/entropice/dashboard/plots/inference.py
index 57ba9f9..26d1adc 100644
--- a/src/entropice/dashboard/plots/inference.py
+++ b/src/entropice/dashboard/plots/inference.py
@@ -6,10 +6,74 @@ import plotly.graph_objects as go
import pydeck as pdk
import streamlit as st
-from entropice.dashboard.utils.class_ordering import get_ordered_classes, sort_class_series
from entropice.dashboard.utils.colors import get_palette
from entropice.dashboard.utils.geometry import fix_hex_geometry
from entropice.dashboard.utils.loaders import TrainingResult
+from entropice.utils.types import Task
+
+# Canonical orderings imported from the ML pipeline
+# Binary labels are defined inline in dataset.py: {False: "No RTS", True: "RTS"}
+# Count/Density labels are defined in the bin_values function
+BINARY_LABELS = ["No RTS", "RTS"]
+COUNT_LABELS = ["None", "Very Few", "Few", "Several", "Many", "Very Many"]
+DENSITY_LABELS = ["Empty", "Very Sparse", "Sparse", "Moderate", "Dense", "Very Dense"]
+
+CLASS_ORDERINGS: dict[Task | str, list[str]] = {
+ "binary": BINARY_LABELS,
+ "count_regimes": COUNT_LABELS,
+ "density_regimes": DENSITY_LABELS,
+ # Legacy aliases (deprecated)
+ "count": COUNT_LABELS,
+ "density": DENSITY_LABELS,
+}
+
+
+def get_ordered_classes(task: Task | str, available_classes: list[str] | None = None) -> list[str]:
+ """Get properly ordered class labels for a given task.
+
+ This uses the same canonical ordering as defined in the ML dataset module,
+ ensuring consistency between training and inference visualizations.
+
+ Args:
+ task: Task type ('binary', 'count_regimes', 'density_regimes', 'count', 'density').
+ available_classes: Optional list of available classes to filter and order.
+ If None, returns all canonical classes for the task.
+
+ Returns:
+ List of class labels in proper order.
+
+ Examples:
+ >>> get_ordered_classes("binary")
+ ['No RTS', 'RTS']
+ >>> get_ordered_classes("count_regimes", ["None", "Few", "Several"])
+ ['None', 'Few', 'Several']
+
+ """
+ canonical_order = CLASS_ORDERINGS[task]
+
+ if available_classes is None:
+ return canonical_order
+
+ # Filter canonical order to only include available classes, preserving order
+ return [cls for cls in canonical_order if cls in available_classes]
+
+
+def sort_class_series(series: pd.Series, task: Task | str) -> pd.Series:
+ """Sort a pandas Series with class labels according to canonical ordering.
+
+ Args:
+ series: Pandas Series with class labels as index.
+ task: Task type ('binary', 'count_regimes', 'density_regimes', 'count', 'density').
+
+ Returns:
+ Sorted Series with classes in canonical order.
+
+ """
+ available_classes = series.index.tolist()
+ ordered_classes = get_ordered_classes(task, available_classes)
+
+ # Reindex to get proper order
+ return series.reindex(ordered_classes)
def render_inference_statistics(predictions_gdf: gpd.GeoDataFrame, task: str):
@@ -60,7 +124,7 @@ def render_class_distribution_histogram(predictions_gdf: gpd.GeoDataFrame, task:
Args:
predictions_gdf: GeoDataFrame with predictions.
- task: Task type ('binary', 'count', 'density').
+ task: Task type ('binary', 'count_regimes', 'density_regimes', 'count', 'density').
"""
st.subheader("📊 Predicted Class Distribution")
@@ -348,7 +412,7 @@ def render_class_comparison(predictions_gdf: gpd.GeoDataFrame, task: str):
Args:
predictions_gdf: GeoDataFrame with predictions.
- task: Task type ('binary', 'count', 'density').
+ task: Task type ('binary', 'count_regimes', 'density_regimes', 'count', 'density').
"""
st.subheader("🔍 Class Comparison")
diff --git a/src/entropice/dashboard/sections/__init__.py b/src/entropice/dashboard/sections/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/entropice/dashboard/sections/correlations.py b/src/entropice/dashboard/sections/correlations.py
new file mode 100644
index 0000000..a6db561
--- /dev/null
+++ b/src/entropice/dashboard/sections/correlations.py
@@ -0,0 +1,674 @@
+"""Cross-dataset correlation analysis dashboard section."""
+
+from typing import Any, cast
+
+import pandas as pd
+import streamlit as st
+import xarray as xr
+
+from entropice.dashboard.plots.correlations import (
+ create_dendrogram_plot,
+ create_feature_variance_plot,
+ create_full_correlation_heatmap,
+ create_matplotlib_correlation_heatmap,
+ create_mutual_information_matrix,
+ create_pca_biplot,
+ select_top_variable_features,
+)
+from entropice.utils.types import L2SourceDataset
+
+
+def _get_aggregation_dimensions(ds: xr.Dataset) -> list[str]:
+ """Get aggregation dimension names from a dataset.
+
+ Args:
+ ds: Xarray Dataset
+
+ Returns:
+ List of aggregation dimension names
+
+ """
+ return [str(dim) for dim in ds.dims if dim in ("agg", "aggregations")]
+
+
+def _set_all_aggregations_corr(
+ member_datasets: dict[L2SourceDataset, xr.Dataset],
+ members_with_aggs: list[L2SourceDataset],
+ member_agg_dims: dict[L2SourceDataset, list[str]],
+ selected: bool,
+):
+ """Set all aggregation checkboxes to selected or deselected state.
+
+ Args:
+ member_datasets: Dictionary mapping members to their loaded datasets
+ members_with_aggs: List of members that have aggregations
+ member_agg_dims: Dictionary mapping members to aggregation dimensions
+ selected: True to select all, False to deselect all
+
+ """
+ for member in members_with_aggs:
+ ds = member_datasets.get(member)
+ if ds is None:
+ continue
+ agg_dims = member_agg_dims.get(member, [])
+ for agg_dim in agg_dims:
+ if agg_dim in ds.dims:
+ agg_values = ds.coords[agg_dim].to_numpy().tolist()
+ for val in agg_values:
+ st.session_state[f"corr_agg_{member}_{agg_dim}_{val}"] = selected
+
+
+def _set_median_only_aggregations_corr(
+ member_datasets: dict[L2SourceDataset, xr.Dataset],
+ members_with_aggs: list[L2SourceDataset],
+ member_agg_dims: dict[L2SourceDataset, list[str]],
+):
+ """Set only median aggregations to selected, deselect all others.
+
+ Args:
+ member_datasets: Dictionary mapping members to their loaded datasets
+ members_with_aggs: List of members that have aggregations
+ member_agg_dims: Dictionary mapping members to aggregation dimensions
+
+ """
+ for member in members_with_aggs:
+ ds = member_datasets.get(member)
+ if ds is None:
+ continue
+ agg_dims = member_agg_dims.get(member, [])
+ for agg_dim in agg_dims:
+ if agg_dim in ds.dims:
+ agg_values = ds.coords[agg_dim].to_numpy().tolist()
+ for val in agg_values:
+ # Select only if value is or contains 'median'
+ is_median = str(val).lower() == "median" or "median" in str(val).lower()
+ st.session_state[f"corr_agg_{member}_{agg_dim}_{val}"] = is_median
+
+
+def _render_member_aggregation_checkboxes(
+ member: L2SourceDataset,
+ ds: xr.Dataset,
+ agg_dims: list[str],
+ column: Any,
+ dimension_filters: dict[str, dict[str, list[str]]],
+) -> dict[str, dict[str, list[str]]]:
+ """Render checkboxes for a single member's aggregations."""
+ with column:
+ st.markdown(f"**{member}:**")
+
+ for agg_dim in agg_dims:
+ if agg_dim not in ds.dims:
+ continue
+
+ # Get coordinate values
+ agg_values = [str(v) for v in ds.coords[agg_dim].to_numpy().tolist()]
+
+ st.markdown(f"*{agg_dim}:*")
+ selected_vals = []
+
+ for val in agg_values:
+ key = f"corr_agg_{member}_{agg_dim}_{val}"
+
+ if st.checkbox(val, value=True, key=key, help=f"Include {val} from {agg_dim}"):
+ selected_vals.append(val)
+
+ # Store selected values if not all selected
+ if selected_vals and len(selected_vals) < len(agg_values):
+ if member not in dimension_filters:
+ dimension_filters[member] = {}
+ dimension_filters[member][agg_dim] = selected_vals
+
+ return dimension_filters
+
+
+def _render_aggregation_selection(
+ member_datasets: dict[L2SourceDataset, xr.Dataset],
+) -> dict[str, dict[str, list[str]]]:
+ """Render aggregation selection controls for members that have aggregations.
+
+ Args:
+ member_datasets: Dictionary mapping member names to their xarray Datasets
+
+ Returns:
+ Dictionary mapping member names to dimension filters
+
+ """
+ # Find members with aggregations
+ members_with_aggs = []
+ member_agg_dims = {}
+
+ for member, ds in member_datasets.items():
+ agg_dims = _get_aggregation_dimensions(ds)
+ if agg_dims:
+ members_with_aggs.append(member)
+ member_agg_dims[member] = agg_dims
+
+ if not members_with_aggs:
+ return {}
+
+ st.markdown("#### Select Aggregations")
+ st.markdown(
+ "Select which spatial aggregations to include. "
+ "This allows you to analyze correlations for specific aggregation types (e.g., mean, median, std)."
+ )
+
+ # Add buttons for common selections (outside form to allow state manipulation)
+ col_btn1, col_btn2, col_btn3, _ = st.columns([1, 1, 1, 3])
+
+ with col_btn1:
+ if st.button("✅ Select All", use_container_width=True, key="corr_agg_select_all"):
+ _set_all_aggregations_corr(member_datasets, members_with_aggs, member_agg_dims, selected=True)
+ with col_btn2:
+ if st.button("📊 Median Only", use_container_width=True, key="corr_agg_median_only"):
+ _set_median_only_aggregations_corr(member_datasets, members_with_aggs, member_agg_dims)
+ with col_btn3:
+ if st.button("❌ Deselect All", use_container_width=True, key="corr_agg_deselect_all"):
+ _set_all_aggregations_corr(member_datasets, members_with_aggs, member_agg_dims, selected=False)
+
+ # Render form with checkboxes
+ with st.form("correlation_aggregation_selection_form"):
+ dimension_filters: dict[str, dict[str, list[str]]] = {}
+
+ # Create columns for each member
+ member_cols = st.columns(len(members_with_aggs))
+
+ for col_idx, member in enumerate(members_with_aggs):
+ dimension_filters = _render_member_aggregation_checkboxes(
+ member,
+ member_datasets[member],
+ member_agg_dims[member],
+ member_cols[col_idx],
+ dimension_filters,
+ )
+
+ # Submit button for the form
+ submitted = st.form_submit_button("Apply Aggregation Filters", type="primary", use_container_width=True)
+
+ if not submitted:
+ st.info("👆 Click 'Apply Aggregation Filters' to update the configuration")
+ st.stop()
+
+ return dimension_filters
+
+
+def _flatten_dataset_to_series(
+ ds: xr.Dataset,
+ prefix: str = "",
+ dimension_filter: dict[str, list[str]] | None = None,
+) -> dict[str, pd.Series]:
+ """Flatten an xarray Dataset into a dict of pandas Series.
+
+ Handles multi-dimensional variables by creating separate series for each combination
+ of non-cell_ids dimensions.
+
+ Args:
+ ds: Xarray Dataset to flatten
+ prefix: Prefix to add to variable names
+ dimension_filter: Optional dict mapping dimension names to lists of allowed values.
+ Only combinations matching these values will be included.
+
+ Returns:
+ Dictionary mapping variable names to pandas Series with cell_ids as index
+
+ """
+ series_dict = {}
+ dimension_filter = dimension_filter or {}
+
+ for var_name in ds.data_vars:
+ var_data = ds[var_name]
+
+ # Get dimensions other than cell_ids
+ other_dims = [dim for dim in var_data.dims if dim != "cell_ids"]
+
+ if len(other_dims) == 0:
+ # Simple 1D variable
+ series = var_data.to_series()
+ full_name = f"{prefix}{var_name}" if prefix else var_name
+ series_dict[full_name] = series
+ else:
+ # Multi-dimensional variable - create series for each combination
+ for coord_values in var_data.stack(stacked=other_dims).coords["stacked"].to_numpy(): # noqa: PD013
+ # Create selector dict
+ if isinstance(coord_values, tuple):
+ selector = dict(zip(other_dims, coord_values))
+ else:
+ selector = {other_dims[0]: coord_values}
+
+ # Check if this combination passes the dimension filter
+ if dimension_filter:
+ skip = False
+ for dim, val in selector.items():
+ if dim in dimension_filter:
+ # Check if value is in allowed list (convert to string for comparison)
+ if str(val) not in dimension_filter[dim]:
+ skip = True
+ break
+ if skip:
+ continue
+
+ # Extract 1D series
+ series = var_data.sel(selector).to_series()
+
+ # Create descriptive name
+ suffix = "_".join(str(v) for v in (coord_values if isinstance(coord_values, tuple) else [coord_values]))
+ full_name = f"{prefix}{var_name}_{suffix}" if prefix else f"{var_name}_{suffix}"
+ series_dict[full_name] = series
+
+ return series_dict
+
+
+@st.fragment
+def _render_correlation_matrix(data_dict: dict[str, pd.Series]):
+ """Render correlation matrix visualization.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series
+
+ """
+ st.subheader("Correlation Matrix")
+
+ n_features = len(data_dict)
+
+ # Add feature reduction options for large datasets
+ reduced_data = data_dict
+ if n_features > 500:
+ st.warning(
+ f"⚠️ **Large dataset detected:** {n_features} features may be too many to visualize effectively. "
+ "Consider reducing the feature set using the options below."
+ )
+
+ with st.expander("🎯 Feature Reduction Options", expanded=True):
+ reduction_method = st.radio(
+ "Reduction Strategy",
+ options=["top_variable", "none"],
+ format_func=lambda x: (
+ "Select Most Variable Features" if x == "top_variable" else "Use All Features (may be slow)"
+ ),
+ index=0,
+ key="corr_reduction_method",
+ )
+
+ if reduction_method == "top_variable":
+ col1, col2 = st.columns(2)
+ with col1:
+ n_top = st.slider(
+ "Number of Features to Keep",
+ min_value=50,
+ max_value=min(1000, n_features),
+ value=min(500, n_features),
+ step=50,
+ key="corr_n_top",
+ )
+ with col2:
+ variability_metric = st.selectbox(
+ "Variability Metric",
+ options=["variance", "iqr", "cv"],
+ format_func=lambda x: {
+ "variance": "Variance",
+ "iqr": "Interquartile Range",
+ "cv": "Coefficient of Variation",
+ }[x],
+ key="corr_variability_metric",
+ )
+
+ reduced_data = select_top_variable_features(data_dict, n_features=n_top, method=variability_metric)
+ st.success(f"✅ Reduced from {n_features} to {len(reduced_data)} most variable features")
+
+ n_reduced = len(reduced_data)
+
+ # Configuration options
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ method = st.selectbox(
+ "Correlation Method",
+ options=["pearson", "spearman", "kendall"],
+ index=0,
+ format_func=lambda x: x.title(),
+ help="Pearson: linear relationships | Spearman: monotonic relationships | Kendall: robust to outliers",
+ key="corr_method",
+ )
+
+ with col2:
+ cluster = cast(
+ bool,
+ st.toggle(
+ "Cluster Variables",
+ value=n_reduced < 100,
+ help="Reorder by hierarchical clustering",
+ key="corr_cluster",
+ ),
+ )
+
+ # Check if subsampling will occur
+ df_test = pd.DataFrame(reduced_data).dropna()
+ if len(df_test) > 50000:
+ st.info(
+ f"📊 **Dataset subsampled:** Using 50,000 randomly selected cells out of {len(df_test):,} "
+ "for performance. Correlations remain representative."
+ )
+
+ # Use matplotlib for large feature sets (> 200), plotly for smaller
+ use_matplotlib = n_reduced > 200
+
+ if use_matplotlib:
+ st.info(
+ f"📈 **Using Matplotlib rendering** for {n_reduced} features "
+ "(Plotly would exceed Streamlit's message size limit). "
+ "This produces a static image but handles large datasets efficiently."
+ )
+ fig = create_matplotlib_correlation_heatmap(reduced_data, method=method, cluster=cluster)
+ st.pyplot(fig, use_container_width=True)
+ # Close figure to free memory
+ import matplotlib.pyplot as plt_cleanup
+
+ plt_cleanup.close(fig)
+ else:
+ fig = create_full_correlation_heatmap(reduced_data, method=method, cluster=cluster)
+ st.plotly_chart(fig, width="stretch")
+
+ st.markdown(
+ f"""
+ **{method.title()} correlation** measures the {"linear" if method == "pearson" else "monotonic"}
+ relationship between variables. Values range from -1 (perfect negative correlation) to
+ +1 (perfect positive correlation), with 0 indicating no correlation.
+ """
+ )
+
+
+@st.fragment
+def _render_pca_analysis(data_dict: dict[str, pd.Series]):
+ """Render PCA biplot visualization.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series
+
+ """
+ st.subheader("Principal Component Analysis (PCA)")
+
+ n_features = len(data_dict)
+
+ # Reduce features if too many for visualization
+ reduced_data = data_dict
+ if n_features > 500:
+ st.warning(
+ f"⚠️ **Large dataset:** {n_features} features detected. "
+ "Automatically selecting the 500 most variable features for PCA visualization."
+ )
+ reduced_data = select_top_variable_features(data_dict, n_features=500, method="variance")
+
+ show_loadings = cast(
+ bool,
+ st.toggle(
+ "Show Loading Vectors",
+ value=True,
+ help="Display how each variable contributes to the principal components",
+ key="pca_loadings",
+ ),
+ )
+
+ fig = create_pca_biplot(reduced_data, n_components=2, show_loadings=show_loadings)
+ st.plotly_chart(fig, width="stretch")
+
+ st.markdown(
+ """
+ **PCA** reduces the dimensionality of the data while preserving variance.
+ - **Blue points**: Individual grid cells projected onto the first two principal components
+ - **Red arrows**: Loading vectors showing how each variable contributes to the PCs
+ - Variables pointing in similar directions are positively correlated
+ - Longer arrows indicate variables with higher variance
+ """
+ )
+
+
+@st.fragment
+def _render_hierarchical_clustering(data_dict: dict[str, pd.Series]):
+ """Render hierarchical clustering dendrogram.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series
+
+ """
+ st.subheader("Hierarchical Clustering of Variables")
+
+ n_features = len(data_dict)
+
+ # Reduce features if too many
+ reduced_data = data_dict
+ if n_features > 300:
+ st.warning(
+ f"⚠️ **Large dataset:** {n_features} features detected. "
+ "Automatically selecting the 300 most variable features for dendrogram visualization."
+ )
+ reduced_data = select_top_variable_features(data_dict, n_features=300, method="variance")
+
+ method = st.selectbox(
+ "Distance Metric (based on correlation)",
+ options=["pearson", "spearman"],
+ index=0,
+ format_func=lambda x: x.title(),
+ key="dendro_method",
+ )
+
+ fig = create_dendrogram_plot(reduced_data, method=method)
+ st.plotly_chart(fig, width="stretch")
+
+ st.markdown(
+ """
+ **Dendrogram** shows hierarchical relationships between variables based on correlation distance.
+ - Variables that merge at lower heights are more similar (highly correlated)
+ - Distinct clusters suggest groups of related features
+ - Useful for identifying redundant features or feature groups
+ """
+ )
+
+
+@st.fragment
+def _render_mutual_information(data_dict: dict[str, pd.Series]):
+ """Render mutual information matrix.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series
+
+ """
+ st.subheader("Mutual Information Analysis")
+
+ n_features = len(data_dict)
+
+ # Mutual information is very computationally expensive - hard limit
+ if n_features > 200:
+ st.error(
+ f"❌ **Too many features:** Mutual information analysis with {n_features} features "
+ "would be too computationally expensive. Please reduce to ≤200 features using the "
+ "variable selection or aggregation filters."
+ )
+ return
+
+ st.warning(
+ "⚠️ **Computationally intensive**: This analysis may take some time. "
+ "Dataset is subsampled to 10,000 cells for performance."
+ )
+
+ fig = create_mutual_information_matrix(data_dict)
+ st.plotly_chart(fig, width="stretch")
+
+ st.markdown(
+ """
+ **Mutual Information** captures both linear and non-linear relationships between variables.
+ - Unlike correlation, MI can detect complex dependencies
+ - Higher values indicate stronger information sharing
+ - Particularly useful for identifying non-linear relationships that correlation might miss
+ """
+ )
+
+
+@st.fragment
+def _render_variance_analysis(data_dict: dict[str, pd.Series]):
+ """Render feature variance and spread analysis.
+
+ Args:
+ data_dict: Dictionary mapping variable names to pandas Series
+
+ """
+ st.subheader("Feature Variance and Spread")
+
+ fig = create_feature_variance_plot(data_dict)
+ st.plotly_chart(fig, width="stretch")
+
+ st.markdown(
+ """
+ **Variance metrics** show the spread and variability of each feature:
+ - **Standard Deviation**: Average distance from the mean
+ - **IQR** (Interquartile Range): Spread of the middle 50% of data (robust to outliers)
+ - **Range**: Difference between max and min values
+ - **CV** (Coefficient of Variation): Normalized variability (std/mean)
+
+ Features with very low variance may provide little information for modeling.
+ """
+ )
+
+
+def _get_variable_groups(all_series: dict[str, pd.Series]) -> dict[str, list[str]]:
+ """Group variables by data source.
+
+ Args:
+ all_series: Dictionary of all variable series
+
+ Returns:
+ Dictionary mapping source names to lists of variable names
+
+ """
+ var_groups = {}
+ for var_name in all_series.keys():
+ source = var_name.split("_")[0]
+ if source not in var_groups:
+ var_groups[source] = []
+ var_groups[source].append(var_name)
+ return var_groups
+
+
+def _render_variable_selection(all_series: dict[str, pd.Series]) -> dict[str, pd.Series]:
+ """Render variable selection UI and return filtered data.
+
+ Args:
+ all_series: Dictionary of all variable series
+
+ Returns:
+ Filtered dictionary with only selected variables
+
+ """
+ var_groups = _get_variable_groups(all_series)
+ selected_vars = []
+
+ # Create checkboxes for each group
+ for source, vars_in_group in sorted(var_groups.items()):
+ with st.container():
+ col1, col2 = st.columns([1, 4])
+ with col1:
+ select_all = st.checkbox(f"**{source}** ({len(vars_in_group)})", value=True, key=f"select_{source}")
+
+ with col2:
+ if select_all:
+ selected_vars.extend(vars_in_group)
+ else:
+ # Show first few variables as examples
+ st.caption(", ".join(vars_in_group[:3]) + ("..." if len(vars_in_group) > 3 else ""))
+
+ # Filter data dict
+ filtered_data = {k: v for k, v in all_series.items() if k in selected_vars}
+
+ if len(filtered_data) < 2:
+ st.warning("⚠️ Please select at least 2 variables for correlation analysis")
+ st.stop()
+
+ st.info(f"**Selected {len(filtered_data)} variables** for analysis")
+ return filtered_data
+
+
+@st.fragment
+def render_correlations_tab(
+ member_datasets: dict[L2SourceDataset, xr.Dataset],
+ grid_area_series: pd.Series | None = None,
+):
+ """Render the cross-dataset correlation analysis tab.
+
+ Args:
+ member_datasets: Dictionary mapping member names to their xarray Datasets
+ grid_area_series: Optional pandas Series with cell_ids as index and area values
+
+ """
+ st.markdown(
+ """
+ This section provides comprehensive analysis of relationships and similarities
+ between all variables across different data sources (ArcticDEM, ERA5, AlphaEarth, etc.).
+ """
+ )
+
+ # Flatten all datasets into a single dict of pandas Series
+ with st.spinner("Preparing data for correlation analysis..."):
+ # First, compute all datasets if needed
+ computed_datasets = {}
+ for member_name, ds in member_datasets.items():
+ # Compute if lazy
+ if any(isinstance(v.data, type(ds)) for v in ds.data_vars.values()):
+ with st.spinner(f"Loading {member_name} data..."):
+ ds = ds.compute()
+ computed_datasets[member_name] = ds
+
+ # Render aggregation selection
+ with st.expander("🔧 Aggregation Selection", expanded=False):
+ dimension_filters = _render_aggregation_selection(computed_datasets)
+
+ if dimension_filters:
+ st.info(
+ f"**Filtering applied:** {sum(len(dims) for dims in dimension_filters.values())} "
+ f"dimension filter(s) across {len(dimension_filters)} data source(s)"
+ )
+
+ # Now flatten with filters applied
+ with st.spinner("Flattening datasets..."):
+ all_series = {}
+
+ # Add grid area if provided
+ if grid_area_series is not None:
+ all_series["Grid_Cell_Area_km2"] = grid_area_series
+
+ # Process each member dataset with its dimension filter
+ for member_name, ds in computed_datasets.items():
+ prefix = f"{member_name}_"
+ member_filter = dimension_filters.get(member_name, None)
+ member_series = _flatten_dataset_to_series(ds, prefix=prefix, dimension_filter=member_filter)
+ all_series.update(member_series)
+
+ st.success(f"✅ Loaded {len(all_series)} variables from {len(member_datasets)} data sources")
+
+ # Variable selection
+ with st.expander("🔧 Variable Selection", expanded=False):
+ st.markdown("Select which variables to include in the correlation analysis.")
+ filtered_data = _render_variable_selection(all_series)
+
+ # Create tabs for different analyses
+ analysis_tabs = st.tabs(
+ [
+ "📊 Correlation Matrix",
+ "🔬 PCA Biplot",
+ "🌳 Hierarchical Clustering",
+ "📈 Variance Analysis",
+ "🔗 Mutual Information",
+ ]
+ )
+
+ with analysis_tabs[0]:
+ _render_correlation_matrix(filtered_data)
+
+ with analysis_tabs[1]:
+ _render_pca_analysis(filtered_data)
+
+ with analysis_tabs[2]:
+ _render_hierarchical_clustering(filtered_data)
+
+ with analysis_tabs[3]:
+ _render_variance_analysis(filtered_data)
+
+ with analysis_tabs[4]:
+ _render_mutual_information(filtered_data)
diff --git a/src/entropice/dashboard/sections/experiment_feature_importance.py b/src/entropice/dashboard/sections/experiment_feature_importance.py
new file mode 100644
index 0000000..8e4f06a
--- /dev/null
+++ b/src/entropice/dashboard/sections/experiment_feature_importance.py
@@ -0,0 +1,331 @@
+"""Feature importance analysis section for experiment comparison."""
+
+import pandas as pd
+import streamlit as st
+
+from entropice.dashboard.plots.experiment_comparison import (
+ create_data_source_importance_bars,
+ create_feature_consistency_plot,
+ create_feature_importance_by_grid_level,
+)
+from entropice.dashboard.utils.loaders import (
+ AutogluonTrainingResult,
+ TrainingResult,
+)
+
+
+def _extract_feature_importance_from_results(
+ training_results: list[TrainingResult],
+) -> pd.DataFrame:
+ """Extract feature importance from all training results.
+
+ Args:
+ training_results: List of TrainingResult objects
+
+ Returns:
+ DataFrame with columns: feature, importance, model, grid, level, task, target
+
+ """
+ records = []
+
+ for tr in training_results:
+ # Load model state if available
+ model_state = tr.load_model_state()
+ if model_state is None:
+ continue
+
+ info = tr.display_info
+
+ # Extract feature importance based on available data
+ if "feature_importance" in model_state.data_vars:
+ # eSPA or similar models with direct feature importance
+ importance_data = model_state["feature_importance"]
+ for feature_idx, feature_name in enumerate(importance_data.coords["feature"].values):
+ importance_value = float(importance_data.isel(feature=feature_idx).values)
+ records.append(
+ {
+ "feature": str(feature_name),
+ "importance": importance_value,
+ "model": info.model,
+ "grid": info.grid,
+ "level": info.level,
+ "task": info.task,
+ "target": info.target,
+ }
+ )
+ elif "gain" in model_state.data_vars:
+ # XGBoost-style feature importance
+ gain_data = model_state["gain"]
+ for feature_idx, feature_name in enumerate(gain_data.coords["feature"].values):
+ importance_value = float(gain_data.isel(feature=feature_idx).values)
+ records.append(
+ {
+ "feature": str(feature_name),
+ "importance": importance_value,
+ "model": info.model,
+ "grid": info.grid,
+ "level": info.level,
+ "task": info.task,
+ "target": info.target,
+ }
+ )
+ elif "feature_importances_" in model_state.data_vars:
+ # Random Forest style
+ importance_data = model_state["feature_importances_"]
+ for feature_idx, feature_name in enumerate(importance_data.coords["feature"].values):
+ importance_value = float(importance_data.isel(feature=feature_idx).values)
+ records.append(
+ {
+ "feature": str(feature_name),
+ "importance": importance_value,
+ "model": info.model,
+ "grid": info.grid,
+ "level": info.level,
+ "task": info.task,
+ "target": info.target,
+ }
+ )
+
+ return pd.DataFrame(records)
+
+
+def _extract_feature_importance_from_autogluon(
+ autogluon_results: list[AutogluonTrainingResult],
+) -> pd.DataFrame:
+ """Extract feature importance from AutoGluon results.
+
+ Args:
+ autogluon_results: List of AutogluonTrainingResult objects
+
+ Returns:
+ DataFrame with columns: feature, importance, model, grid, level, task, target
+
+ """
+ records = []
+
+ for ag in autogluon_results:
+ if ag.feature_importance is None:
+ continue
+
+ info = ag.display_info
+
+ # AutoGluon feature importance is already a DataFrame with features as index
+ for feature_name, importance_value in ag.feature_importance["importance"].items():
+ records.append(
+ {
+ "feature": str(feature_name),
+ "importance": float(importance_value),
+ "model": "autogluon",
+ "grid": info.grid,
+ "level": info.level,
+ "task": info.task,
+ "target": info.target,
+ }
+ )
+
+ return pd.DataFrame(records)
+
+
+def _categorize_feature(feature_name: str) -> str:
+ """Categorize feature by data source."""
+ feature_lower = feature_name.lower()
+ if feature_lower.startswith("arcticdem"):
+ return "ArcticDEM"
+ if feature_lower.startswith("era5"):
+ return "ERA5"
+ if feature_lower.startswith("embeddings") or feature_lower.startswith("alphaearth"):
+ return "Embeddings"
+ return "General"
+
+
+def _prepare_feature_importance_data(
+ training_results: list[TrainingResult],
+ autogluon_results: list[AutogluonTrainingResult],
+) -> pd.DataFrame | None:
+ """Extract and prepare feature importance data.
+
+ Args:
+ training_results: List of RandomSearchCV training results
+ autogluon_results: List of AutoGluon training results
+
+ Returns:
+ DataFrame with feature importance data or None if no data available
+
+ """
+ fi_df_cv = _extract_feature_importance_from_results(training_results)
+ fi_df_ag = _extract_feature_importance_from_autogluon(autogluon_results)
+
+ if fi_df_cv.empty and fi_df_ag.empty:
+ return None
+
+ # Combine both
+ fi_df = pd.concat([fi_df_cv, fi_df_ag], ignore_index=True)
+
+ # Add data source categorization
+ fi_df["data_source"] = fi_df["feature"].apply(_categorize_feature)
+ fi_df["grid_level"] = fi_df["grid"] + "_" + fi_df["level"].astype(str)
+
+ return fi_df
+
+
+@st.fragment
+def render_feature_importance_analysis(
+ training_results: list[TrainingResult],
+ autogluon_results: list[AutogluonTrainingResult],
+):
+ """Render feature importance analysis section.
+
+ Args:
+ training_results: List of RandomSearchCV training results
+ autogluon_results: List of AutoGluon training results
+
+ """
+ st.header("🔍 Feature Importance Analysis")
+
+ st.markdown(
+ """
+ This section analyzes which features are most important across different
+ models, grid levels, tasks, and targets.
+ """
+ )
+
+ # Extract feature importance
+ with st.spinner("Extracting feature importance from training results..."):
+ fi_df = _prepare_feature_importance_data(training_results, autogluon_results)
+
+ if fi_df is None:
+ st.warning("No feature importance data available. Model state files may be missing.")
+ return
+
+ st.success(f"Extracted feature importance from {len(fi_df)} feature-model combinations")
+
+ # Filters
+ st.subheader("Filters")
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ # Task filter
+ available_tasks = ["All", *sorted(fi_df["task"].unique().tolist())]
+ selected_task = st.selectbox("Task", options=available_tasks, index=0, key="fi_task_filter")
+
+ with col2:
+ # Target filter
+ available_targets = ["All", *sorted(fi_df["target"].unique().tolist())]
+ selected_target = st.selectbox("Target Dataset", options=available_targets, index=0, key="fi_target_filter")
+
+ with col3:
+ # Top N features
+ top_n_features = st.number_input("Top N Features", min_value=5, max_value=50, value=15, key="top_n_features")
+
+ # Apply filters
+ filtered_fi_df = fi_df.copy()
+ if selected_task != "All":
+ filtered_fi_df = filtered_fi_df.loc[filtered_fi_df["task"] == selected_task]
+ if selected_target != "All":
+ filtered_fi_df = filtered_fi_df.loc[filtered_fi_df["target"] == selected_target]
+
+ if len(filtered_fi_df) == 0:
+ st.warning("No feature importance data available for the selected filters.")
+ return
+
+ # Section 1: Top features by grid level
+ st.subheader("Top Features by Grid Level")
+
+ try:
+ fig = create_feature_importance_by_grid_level(filtered_fi_df, top_n=top_n_features)
+ st.plotly_chart(fig, width="stretch")
+ except Exception as e:
+ st.error(f"Could not create feature importance by grid level plot: {e}")
+
+ # Show detailed breakdown in expander
+ grid_levels = sorted(filtered_fi_df["grid_level"].unique())
+
+ with st.expander("Show Detailed Breakdown by Grid Level", expanded=False):
+ for grid_level in grid_levels:
+ grid_data = filtered_fi_df[filtered_fi_df["grid_level"] == grid_level]
+
+ # Get top features for this grid level
+ top_features_grid = (
+ grid_data.groupby("feature")["importance"].mean().reset_index().nlargest(top_n_features, "importance")
+ )
+
+ st.markdown(f"**{grid_level.replace('_', '-').title()}**")
+
+ # Create display dataframe with data source
+ display_df = top_features_grid.merge(
+ grid_data[["feature", "data_source"]].drop_duplicates(), on="feature", how="left"
+ )
+ display_df.columns = ["Feature", "Mean Importance", "Data Source"]
+ display_df = display_df.sort_values("Mean Importance", ascending=False)
+
+ st.dataframe(display_df, width="stretch", hide_index=True)
+
+ # Section 2: Feature importance consistency across models
+ st.subheader("Feature Importance Consistency Across Models")
+
+ st.markdown(
+ """
+ **Coefficient of Variation (CV)**: Lower values indicate more consistent importance across models.
+ High CV suggests the feature's importance varies significantly between different models.
+ """
+ )
+
+ try:
+ fig = create_feature_consistency_plot(filtered_fi_df, top_n=top_n_features)
+ st.plotly_chart(fig, width="stretch")
+ except Exception as e:
+ st.error(f"Could not create feature consistency plot: {e}")
+
+ # Show detailed statistics in expander
+ with st.expander("Show Detailed Statistics", expanded=False):
+ # Get top features overall
+ overall_top_features = (
+ filtered_fi_df.groupby("feature")["importance"]
+ .mean()
+ .reset_index()
+ .nlargest(top_n_features, "importance")["feature"]
+ .tolist()
+ )
+
+ # Calculate variance in importance across models for each feature
+ feature_variance = (
+ filtered_fi_df[filtered_fi_df["feature"].isin(overall_top_features)]
+ .groupby("feature")["importance"]
+ .agg(["mean", "std", "min", "max"])
+ .reset_index()
+ )
+ feature_variance["coefficient_of_variation"] = feature_variance["std"] / feature_variance["mean"]
+ feature_variance = feature_variance.sort_values("mean", ascending=False)
+
+ # Add data source
+ feature_variance = feature_variance.merge(
+ filtered_fi_df[["feature", "data_source"]].drop_duplicates(), on="feature", how="left"
+ )
+
+ feature_variance.columns = ["Feature", "Mean", "Std Dev", "Min", "Max", "CV", "Data Source"]
+
+ st.dataframe(
+ feature_variance[["Feature", "Data Source", "Mean", "Std Dev", "CV"]],
+ width="stretch",
+ hide_index=True,
+ )
+
+ # Section 3: Feature importance by data source
+ st.subheader("Feature Importance by Data Source")
+
+ try:
+ fig = create_data_source_importance_bars(filtered_fi_df)
+ st.plotly_chart(fig, width="stretch")
+ except Exception as e:
+ st.error(f"Could not create data source importance chart: {e}")
+
+ # Show detailed table in expander
+ with st.expander("Show Data Source Statistics", expanded=False):
+ # Aggregate importance by data source
+ source_importance = (
+ filtered_fi_df.groupby("data_source")["importance"].agg(["sum", "mean", "count"]).reset_index()
+ )
+ source_importance.columns = ["Data Source", "Total Importance", "Mean Importance", "Feature Count"]
+ source_importance = source_importance.sort_values("Total Importance", ascending=False)
+
+ st.dataframe(source_importance, width="stretch", hide_index=True)
diff --git a/src/entropice/dashboard/sections/experiment_grid_analysis.py b/src/entropice/dashboard/sections/experiment_grid_analysis.py
new file mode 100644
index 0000000..f83bdcc
--- /dev/null
+++ b/src/entropice/dashboard/sections/experiment_grid_analysis.py
@@ -0,0 +1,75 @@
+"""Grid-level analysis section for experiment comparison."""
+
+import pandas as pd
+import streamlit as st
+
+from entropice.dashboard.plots.experiment_comparison import create_grid_level_comparison_plot
+from entropice.dashboard.utils.formatters import format_metric_name
+
+
+@st.fragment
+def render_grid_level_analysis(summary_df: pd.DataFrame, available_metrics: list[str]):
+ """Render grid-level analysis section.
+
+ Args:
+ summary_df: Summary DataFrame with all results
+ available_metrics: List of available metrics
+
+ """
+ st.header("📐 Grid Level Analysis")
+
+ st.markdown(
+ """
+ This section analyzes how different grid levels affect model performance.
+ Compare performance across grid types (hex vs healpix) and resolution levels.
+ Metrics are automatically selected based on the task type.
+ """
+ )
+
+ # Determine metrics to use per task
+ task_metric_map = {
+ "binary": "f1",
+ "count_regimes": "f1_weighted",
+ "density_regimes": "f1_weighted",
+ "count": "r2",
+ "density": "r2",
+ }
+
+ # Get unique tasks in the data
+ unique_tasks = summary_df["task"].unique()
+
+ # Split selection
+ split = st.selectbox("Data Split", options=["test", "train", "combined"], index=0, key="grid_split")
+
+ # Create plots for each task
+ for task in sorted(unique_tasks):
+ # Determine the metric for this task
+ metric = task_metric_map.get(task, available_metrics[0])
+
+ # Check if metric is available
+ metric_col = f"{split}_{metric}"
+ if metric_col not in summary_df.columns:
+ # Fall back to first available metric
+ metric = available_metrics[0]
+ metric_col = f"{split}_{metric}"
+
+ st.subheader(f"{task.replace('_', ' ').title()} - {format_metric_name(metric)}")
+
+ # Filter data for this task
+ task_df = summary_df[summary_df["task"] == task]
+
+ # Create grid-level comparison plot
+ try:
+ fig = create_grid_level_comparison_plot(task_df, metric, split)
+ st.plotly_chart(fig, width="stretch")
+ except Exception as e:
+ st.error(f"Could not create grid-level comparison plot for {task}: {e}")
+
+ # Show statistics by grid level for this task
+ if metric_col in task_df.columns:
+ stats = (
+ task_df.groupby(["grid", "level"])[metric_col].agg(["mean", "std", "min", "max", "count"]).reset_index()
+ )
+ stats.columns = ["Grid", "Level", "Mean", "Std Dev", "Min", "Max", "Count"]
+ with st.expander(f"Show {format_metric_name(metric)} Statistics by Grid Level", expanded=False):
+ st.dataframe(stats.sort_values("Mean", ascending=False), width="stretch", hide_index=True)
diff --git a/src/entropice/dashboard/sections/experiment_inference_maps.py b/src/entropice/dashboard/sections/experiment_inference_maps.py
new file mode 100644
index 0000000..a2570f4
--- /dev/null
+++ b/src/entropice/dashboard/sections/experiment_inference_maps.py
@@ -0,0 +1,193 @@
+"""Section for visualizing experiment inference maps."""
+
+import streamlit as st
+
+from entropice.dashboard.plots.experiment_comparison import create_inference_maps
+from entropice.dashboard.utils.loaders import TrainingResult
+from entropice.utils.types import GridConfig
+
+
+@st.fragment
+def render_inference_maps_section(
+ experiment_name: str,
+ training_results: list[TrainingResult],
+) -> None:
+ """Render the inference maps section.
+
+ Args:
+ experiment_name: Name of the experiment
+ training_results: List of training results for the experiment
+
+ """
+ st.header("🗺️ Inference Maps Analysis")
+
+ st.markdown(
+ """
+ Visualize the mean and uncertainty (standard deviation) of model predictions across the Arctic region.
+ The maps show the spatial distribution of predictions aggregated across multiple training runs.
+ """
+ )
+
+ if not training_results:
+ st.info("No training results available for inference maps.")
+ return
+
+ # Extract unique grid configurations from training results
+ available_grid_configs = sorted(
+ {GridConfig.from_grid_level((tr.settings.grid, tr.settings.level)) for tr in training_results},
+ key=lambda gc: gc.sort_key,
+ )
+ available_tasks = sorted({tr.settings.task for tr in training_results})
+ available_targets = sorted({tr.settings.target for tr in training_results})
+ available_models = sorted({tr.settings.model for tr in training_results})
+
+ # Create form for selecting parameters
+ with st.form("inference_map_form"):
+ st.subheader("Map Configuration")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ selected_grid_config = st.selectbox(
+ "Grid Configuration",
+ options=available_grid_configs,
+ format_func=lambda gc: gc.display_name,
+ help="Select the grid type and resolution level for the inference map",
+ )
+
+ selected_task = st.selectbox(
+ "Task",
+ options=available_tasks,
+ help="Select the prediction task",
+ )
+
+ st.subheader("Filters")
+
+ col3, col4 = st.columns(2)
+
+ with col3:
+ selected_targets = st.multiselect(
+ "Target Datasets",
+ options=available_targets,
+ default=available_targets,
+ help="Filter by target datasets (select all to include all)",
+ )
+
+ with col4:
+ selected_models = st.multiselect(
+ "Model Types",
+ options=available_models,
+ default=available_models,
+ help="Filter by model types (select all to include all)",
+ )
+
+ submit_button = st.form_submit_button("Generate Maps", type="primary")
+
+ if submit_button:
+ # Extract grid and level from selected config
+ selected_grid = selected_grid_config.grid
+ selected_level = selected_grid_config.level
+
+ # Filter training results based on selections
+ filtered_results = [
+ tr
+ for tr in training_results
+ if tr.settings.grid == selected_grid
+ and tr.settings.level == selected_level
+ and tr.settings.task == selected_task
+ and tr.settings.target in selected_targets
+ and tr.settings.model in selected_models
+ ]
+
+ if not filtered_results:
+ st.warning("No training results match the selected criteria. Please adjust your selections and try again.")
+ return
+
+ st.success(f"Found {len(filtered_results)} training runs matching the criteria.")
+
+ # Display metadata about selected runs
+ with st.expander("View Selected Training Runs"):
+ run_info = []
+ for tr in filtered_results:
+ info = tr.display_info
+ run_info.append(
+ {
+ "Task": info.task,
+ "Target": info.target,
+ "Model": info.model,
+ "Grid": f"{info.grid}_{info.level}",
+ "Created": info.timestamp.strftime("%Y-%m-%d %H:%M"),
+ }
+ )
+ st.dataframe(run_info, width="stretch")
+
+ # Calculate inference maps
+ with st.spinner("Calculating inference maps from predictions..."):
+ try:
+ from entropice.dashboard.utils.loaders import TrainingResult
+
+ inference_gdf = TrainingResult.calculate_inference_maps(filtered_results)
+
+ st.info(
+ f"Generated inference map with {len(inference_gdf):,} grid cells. "
+ + (
+ "Using point-based rendering (>50k cells)."
+ if len(inference_gdf) > 50000
+ else "Using polygon-based rendering."
+ )
+ )
+
+ except AssertionError as e:
+ st.error(f"Error calculating inference maps: {e}")
+ return
+ except Exception as e:
+ st.error(f"Unexpected error calculating inference maps: {e}")
+ st.exception(e)
+ return
+
+ # Generate maps
+ with st.spinner("Generating cartographic visualizations..."):
+ try:
+ deck_mean, deck_std = create_inference_maps(
+ inference_gdf,
+ grid=selected_grid,
+ level=selected_level,
+ task=selected_task,
+ )
+
+ # Display maps
+ st.subheader("📍 Mean Prediction Map")
+ st.pydeck_chart(deck_mean, use_container_width=True)
+
+ st.subheader("📍 Uncertainty Map (Standard Deviation)")
+ st.pydeck_chart(deck_std, use_container_width=True)
+
+ # Display statistics
+ st.subheader("📊 Inference Statistics")
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ st.metric("Grid Cells", f"{len(inference_gdf):,}")
+
+ with col2:
+ st.metric(
+ "Mean Prediction",
+ f"{inference_gdf['mean_prediction'].mean():.4f}",
+ )
+
+ with col3:
+ st.metric(
+ "Avg Uncertainty",
+ f"{inference_gdf['std_prediction'].mean():.4f}",
+ )
+
+ with col4:
+ st.metric(
+ "Max Uncertainty",
+ f"{inference_gdf['std_prediction'].max():.4f}",
+ )
+
+ except Exception as e:
+ st.error(f"Error generating maps: {e}")
+ st.exception(e)
+ return
diff --git a/src/entropice/dashboard/sections/experiment_model_comparison.py b/src/entropice/dashboard/sections/experiment_model_comparison.py
new file mode 100644
index 0000000..f1290d2
--- /dev/null
+++ b/src/entropice/dashboard/sections/experiment_model_comparison.py
@@ -0,0 +1,77 @@
+"""Model comparison section for experiment analysis."""
+
+import pandas as pd
+import streamlit as st
+
+from entropice.dashboard.plots.experiment_comparison import create_top_models_bar_chart
+from entropice.dashboard.utils.formatters import format_metric_name
+
+
+@st.fragment
+def render_model_comparison(summary_df: pd.DataFrame, available_metrics: list[str]):
+ """Render model comparison section.
+
+ Args:
+ summary_df: Summary DataFrame with all results
+ available_metrics: List of available metrics
+
+ """
+ st.header("🏆 Model Performance Comparison")
+
+ st.markdown(
+ """
+ This section shows the best performing models for each task.
+ """
+ )
+
+ # Determine metrics to use per task
+ task_metric_map = {
+ "binary": "f1",
+ "count_regimes": "f1_weighted",
+ "density_regimes": "f1_weighted",
+ "count": "r2",
+ "density": "r2",
+ }
+
+ # Split selection
+ split = st.selectbox("Data Split", options=["test", "train", "combined"], index=0, key="model_split")
+
+ # Get unique tasks
+ unique_tasks = summary_df["task"].unique()
+
+ # For each task, show the best models
+ for task in sorted(unique_tasks):
+ # Determine the metric for this task
+ metric = task_metric_map.get(task, available_metrics[0])
+ metric_col = f"{split}_{metric}"
+
+ if metric_col not in summary_df.columns:
+ # Fall back to first available metric
+ metric = available_metrics[0]
+ metric_col = f"{split}_{metric}"
+
+ st.subheader(f"{task.replace('_', ' ').title()}")
+
+ # Filter data for this task
+ task_df = summary_df[summary_df["task"] == task].copy()
+
+ # Create visualization
+ try:
+ fig = create_top_models_bar_chart(task_df, metric, task, split, top_n=10)
+ st.plotly_chart(fig, width="stretch")
+ except Exception as e:
+ st.error(f"Could not create model comparison chart for {task}: {e}")
+
+ # Show table in expander
+ with st.expander("Show Top 10 Models Table", expanded=False):
+ top_models = task_df.nlargest(10, metric_col)[
+ ["model", "grid", "level", "target", metric_col, "method"]
+ ].copy()
+ top_models.columns = ["Model", "Grid", "Level", "Target", format_metric_name(metric), "Method"]
+ st.dataframe(
+ top_models.reset_index(drop=True),
+ width="stretch",
+ hide_index=True,
+ )
+
+ st.divider()
diff --git a/src/entropice/dashboard/sections/experiment_overview.py b/src/entropice/dashboard/sections/experiment_overview.py
new file mode 100644
index 0000000..5b0e1ba
--- /dev/null
+++ b/src/entropice/dashboard/sections/experiment_overview.py
@@ -0,0 +1,92 @@
+"""Experiment overview and sidebar sections."""
+
+import pandas as pd
+import streamlit as st
+
+from entropice.dashboard.utils.loaders import (
+ AutogluonTrainingResult,
+ TrainingResult,
+ get_available_experiments,
+)
+
+
+def render_experiment_sidebar() -> str | None:
+ """Render sidebar for experiment selection.
+
+ Returns:
+ Selected experiment name or None
+
+ """
+ st.sidebar.header("🔬 Experiment Selection")
+
+ experiments = get_available_experiments()
+
+ if not experiments:
+ st.sidebar.warning("No experiments found. Create an experiment directory with training results first.")
+ return None
+
+ selected_experiment = st.sidebar.selectbox(
+ "Select Experiment",
+ options=experiments,
+ index=0,
+ help="Choose an experiment to analyze",
+ )
+
+ return selected_experiment
+
+
+def render_experiment_overview(
+ experiment_name: str,
+ training_results: list[TrainingResult],
+ autogluon_results: list[AutogluonTrainingResult],
+ summary_df: pd.DataFrame,
+):
+ """Render experiment overview section.
+
+ Args:
+ experiment_name: Name of the experiment
+ training_results: List of RandomSearchCV training results
+ autogluon_results: List of AutoGluon training results
+ summary_df: Summary DataFrame with all results
+
+ """
+ st.header(f"📊 Experiment: {experiment_name}")
+
+ # Show summary statistics
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ st.metric("Total Training Runs", len(training_results) + len(autogluon_results))
+
+ with col2:
+ st.metric("RandomSearchCV Runs", len(training_results))
+
+ with col3:
+ st.metric("AutoGluon Runs", len(autogluon_results))
+
+ with col4:
+ unique_configs = summary_df[["grid", "level", "task", "target"]].drop_duplicates()
+ st.metric("Unique Configurations", len(unique_configs))
+
+ st.divider()
+
+ # Show summary table
+ st.subheader("Experiment Summary")
+
+ display_columns = [
+ "method",
+ "task",
+ "target",
+ "model",
+ "grid_level",
+ "test_score",
+ "best_metric",
+ "n_trials",
+ ]
+ available_columns = [col for col in display_columns if col in summary_df.columns]
+
+ st.dataframe(
+ summary_df[available_columns].sort_values("test_score", ascending=False),
+ width="stretch",
+ hide_index=True,
+ )
diff --git a/src/entropice/dashboard/utils/__init__.py b/src/entropice/dashboard/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/entropice/dashboard/utils/loaders.py b/src/entropice/dashboard/utils/loaders.py
index a0e6fbe..72df448 100644
--- a/src/entropice/dashboard/utils/loaders.py
+++ b/src/entropice/dashboard/utils/loaders.py
@@ -8,6 +8,7 @@ from datetime import datetime
from pathlib import Path
import antimeridian
+import geopandas as gpd
import pandas as pd
import streamlit as st
import toml
@@ -19,7 +20,7 @@ import entropice.utils.paths
from entropice.dashboard.utils.formatters import TrainingResultDisplayInfo
from entropice.ml.autogluon_training import AutoGluonTrainingSettings
from entropice.ml.dataset import DatasetEnsemble, TrainingSet
-from entropice.ml.training import TrainingSettings
+from entropice.ml.randomsearch import TrainingSettings
from entropice.utils.types import GridConfig, TargetDataset, Task, all_target_datasets, all_tasks
@@ -215,6 +216,37 @@ class TrainingResult:
records.append(record)
return pd.DataFrame.from_records(records)
+ @staticmethod
+ def calculate_inference_maps(training_results: list["TrainingResult"]) -> gpd.GeoDataFrame:
+ """Calculate the mean and standard deviation of inference maps across multiple training results."""
+ assert len({tr.settings.grid for tr in training_results}) == 1, "All training results must have the same grid"
+ assert len({tr.settings.level for tr in training_results}) == 1, "All training results must have the same level"
+
+ grid = training_results[0].settings.grid
+ level = training_results[0].settings.level
+ gridfile = entropice.utils.paths.get_grid_file(grid, level)
+ cells = gpd.read_parquet(gridfile, columns=["cell_id", "geometry"])
+ if grid == "hex":
+ cells["cell_id"] = cells["cell_id"].apply(lambda x: int(x, 16))
+ cells = cells.set_index("cell_id")
+
+ vals = []
+ for tr in training_results:
+ preds_file = tr.path / "predicted_probabilities.parquet"
+ if not preds_file.exists():
+ continue
+ preds = pd.read_parquet(preds_file, columns=["cell_id", "predicted"]).set_index("cell_id")
+ if preds["predicted"].dtype == "category":
+ preds["predicted"] = preds["predicted"].cat.codes
+ vals.append(preds)
+ all_preds = pd.concat(vals, axis=1)
+ mean_preds = all_preds.mean(axis=1)
+ std_preds = all_preds.std(axis=1)
+ cells["mean_prediction"] = mean_preds
+ cells["std_prediction"] = std_preds
+
+ return cells.reset_index()
+
@st.cache_data(ttl=300) # Cache for 5 minutes
def load_all_training_results() -> list[TrainingResult]:
@@ -410,6 +442,185 @@ def load_training_sets(ensemble: DatasetEnsemble) -> dict[TargetDataset, dict[Ta
return train_data_dict
+def get_available_experiments() -> list[str]:
+ """Get list of available experiment names from the results directory.
+
+ Returns:
+ List of experiment directory names
+
+ """
+ results_dir = entropice.utils.paths.RESULTS_DIR
+ experiments = []
+
+ for item in results_dir.iterdir():
+ if not item.is_dir():
+ continue
+
+ # Check if this directory contains training results (has subdirs with training results files)
+ has_training_results = False
+ for subitem in item.iterdir():
+ if not subitem.is_dir():
+ continue
+ # Check for either RandomSearchCV or AutoGluon result files
+ if (subitem / "search_results.parquet").exists() or (subitem / "leaderboard.parquet").exists():
+ has_training_results = True
+ break
+
+ if has_training_results:
+ experiments.append(item.name)
+
+ return sorted(experiments)
+
+
+def load_experiment_training_results(experiment_name: str) -> list[TrainingResult]:
+ """Load all training results for a specific experiment.
+
+ Args:
+ experiment_name: Name of the experiment directory
+
+ Returns:
+ List of TrainingResult objects for the experiment
+
+ """
+ experiment_dir = entropice.utils.paths.RESULTS_DIR / experiment_name
+ if not experiment_dir.exists():
+ return []
+
+ training_results: list[TrainingResult] = []
+ for result_path in experiment_dir.iterdir():
+ if not result_path.is_dir():
+ continue
+ # Skip AutoGluon results
+ if "autogluon" in result_path.name.lower():
+ continue
+
+ try:
+ training_result = TrainingResult.from_path(result_path, experiment_name)
+ training_results.append(training_result)
+ except FileNotFoundError:
+ pass # Skip incomplete results
+
+ # Sort by creation time (most recent first)
+ training_results.sort(key=lambda tr: tr.created_at, reverse=True)
+ return training_results
+
+
+def load_experiment_autogluon_results(experiment_name: str) -> list[AutogluonTrainingResult]:
+ """Load all AutoGluon training results for a specific experiment.
+
+ Args:
+ experiment_name: Name of the experiment directory
+
+ Returns:
+ List of AutogluonTrainingResult objects for the experiment
+
+ """
+ experiment_dir = entropice.utils.paths.RESULTS_DIR / experiment_name
+ if not experiment_dir.exists():
+ return []
+
+ training_results: list[AutogluonTrainingResult] = []
+ for result_path in experiment_dir.iterdir():
+ if not result_path.is_dir():
+ continue
+ # Only include AutoGluon results
+ if "autogluon" not in result_path.name.lower():
+ continue
+
+ try:
+ training_result = AutogluonTrainingResult.from_path(result_path, experiment_name)
+ training_results.append(training_result)
+ except FileNotFoundError:
+ pass # Skip incomplete results
+
+ # Sort by creation time (most recent first)
+ training_results.sort(key=lambda tr: tr.created_at, reverse=True)
+ return training_results
+
+
+def create_experiment_summary_df(
+ training_results: list[TrainingResult], autogluon_results: list[AutogluonTrainingResult]
+) -> pd.DataFrame:
+ """Create a summary DataFrame for all results in an experiment.
+
+ Args:
+ training_results: List of TrainingResult objects
+ autogluon_results: List of AutogluonTrainingResult objects
+
+ Returns:
+ DataFrame with summary statistics for the experiment
+
+ """
+ records = []
+
+ # Add RandomSearchCV results
+ for tr in training_results:
+ info = tr.display_info
+ best_metric_name = tr._get_best_metric_name()
+
+ record = {
+ "method": "RandomSearchCV",
+ "task": info.task,
+ "target": info.target,
+ "model": info.model,
+ "grid": info.grid,
+ "level": info.level,
+ "grid_level": f"{info.grid}_{info.level}",
+ "train_score": tr.train_metrics.get(best_metric_name, float("nan")),
+ "test_score": tr.test_metrics.get(best_metric_name, float("nan")),
+ "combined_score": tr.combined_metrics.get(best_metric_name, float("nan")),
+ "best_metric": best_metric_name,
+ "n_trials": len(tr.results),
+ "created_at": tr.created_at,
+ "path": tr.path,
+ }
+
+ # Add all train metrics
+ for metric, value in tr.train_metrics.items():
+ record[f"train_{metric}"] = value
+
+ # Add all test metrics
+ for metric, value in tr.test_metrics.items():
+ record[f"test_{metric}"] = value
+
+ # Add all combined metrics
+ for metric, value in tr.combined_metrics.items():
+ record[f"combined_{metric}"] = value
+
+ records.append(record)
+
+ # Add AutoGluon results
+ for ag in autogluon_results:
+ info = ag.display_info
+ best_metric_name = ag._get_best_metric_name()
+
+ record = {
+ "method": "AutoGluon",
+ "task": info.task,
+ "target": info.target,
+ "model": "ensemble", # AutoGluon is an ensemble
+ "grid": info.grid,
+ "level": info.level,
+ "grid_level": f"{info.grid}_{info.level}",
+ "train_score": float("nan"), # AutoGluon doesn't separate train scores
+ "test_score": ag.test_metrics.get(best_metric_name, float("nan")),
+ "combined_score": float("nan"),
+ "best_metric": best_metric_name,
+ "n_trials": len(ag.leaderboard),
+ "created_at": ag.created_at,
+ "path": ag.path,
+ }
+
+ # Add test metrics
+ for metric, value in ag.test_metrics.items():
+ if isinstance(value, (int, float)):
+ record[f"test_{metric}"] = value
+
+ records.append(record)
+
+ return pd.DataFrame(records)
+
+
@dataclass
class StorageInfo:
"""Storage information for a directory."""
diff --git a/src/entropice/dashboard/views/__init__.py b/src/entropice/dashboard/views/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/entropice/dashboard/views/dataset_page.py b/src/entropice/dashboard/views/dataset_page.py
index a6c8340..f4efa27 100644
--- a/src/entropice/dashboard/views/dataset_page.py
+++ b/src/entropice/dashboard/views/dataset_page.py
@@ -138,6 +138,7 @@ def render_dataset_page():
)
if era5_members:
tab_names.append("🌡️ ERA5")
+ tab_names += ["🔗 Correlations"]
tabs = st.tabs(tab_names)
with tabs[0]:
@@ -168,6 +169,12 @@ def render_dataset_page():
era5_member_dataset = {m: member_datasets[m] for m in era5_members}
era5_member_stats = {m: stats.members[m] for m in era5_members}
render_era5_tab(era5_member_dataset, grid_gdf, era5_member_stats)
+ tab_index += 1
+ with tabs[tab_index]:
+ st.header("🔗 Cross-Dataset Correlation Analysis")
+ # Extract grid area series
+ # grid_area_series = grid_gdf.set_index("cell_id")["area_km2"] if "area_km2" in grid_gdf.columns else None
+ # render_correlations_tab(member_datasets, grid_area_series=grid_area_series)
st.balloons()
stopwatch.summary()
diff --git a/src/entropice/dashboard/views/experiment_analysis_page.py b/src/entropice/dashboard/views/experiment_analysis_page.py
new file mode 100644
index 0000000..14af39a
--- /dev/null
+++ b/src/entropice/dashboard/views/experiment_analysis_page.py
@@ -0,0 +1,87 @@
+"""Experiment Analysis page: Compare multiple training runs within an experiment."""
+
+import streamlit as st
+
+from entropice.dashboard.sections.experiment_feature_importance import render_feature_importance_analysis
+from entropice.dashboard.sections.experiment_grid_analysis import render_grid_level_analysis
+from entropice.dashboard.sections.experiment_inference_maps import render_inference_maps_section
+from entropice.dashboard.sections.experiment_model_comparison import render_model_comparison
+from entropice.dashboard.sections.experiment_overview import (
+ render_experiment_overview,
+ render_experiment_sidebar,
+)
+from entropice.dashboard.utils.loaders import (
+ create_experiment_summary_df,
+ load_experiment_autogluon_results,
+ load_experiment_training_results,
+)
+
+
+def render_experiment_analysis_page():
+ """Render the Experiment Analysis page of the dashboard."""
+ st.title("🔬 Experiment Analysis")
+
+ st.markdown(
+ """
+ Analyze and compare multiple training runs within an experiment.
+ Select an experiment from the sidebar to explore:
+ - How grid levels affect performance
+ - Best models across different tasks and targets
+ - Feature importance patterns
+ """
+ )
+
+ # Experiment selection
+ selected_experiment = render_experiment_sidebar()
+
+ if selected_experiment is None:
+ st.info("👈 Select an experiment from the sidebar to begin analysis.")
+ st.stop()
+
+ assert selected_experiment is not None, "Experiment must be selected"
+
+ # Load experiment results
+ with st.spinner(f"Loading results for experiment: {selected_experiment}..."):
+ training_results = load_experiment_training_results(selected_experiment)
+ autogluon_results = load_experiment_autogluon_results(selected_experiment)
+
+ if not training_results and not autogluon_results:
+ st.warning(f"No training results found in experiment: {selected_experiment}")
+ st.stop()
+
+ # Create summary DataFrame
+ summary_df = create_experiment_summary_df(training_results, autogluon_results)
+
+ # Get available metrics
+ metric_columns = [col for col in summary_df.columns if col.startswith("test_")]
+ available_metrics = [col.replace("test_", "") for col in metric_columns]
+
+ if not available_metrics:
+ st.error("No metrics found in the experiment results.")
+ st.stop()
+
+ # Render analysis sections
+ render_experiment_overview(selected_experiment, training_results, autogluon_results, summary_df)
+
+ st.divider()
+
+ render_grid_level_analysis(summary_df, available_metrics)
+
+ st.divider()
+
+ render_model_comparison(summary_df, available_metrics)
+
+ st.divider()
+
+ render_feature_importance_analysis(training_results, autogluon_results)
+
+ st.divider()
+
+ render_inference_maps_section(selected_experiment, training_results)
+
+ st.divider()
+
+ # Raw data section
+ st.header("📄 Raw Experiment Data")
+ with st.expander("View Complete Summary DataFrame"):
+ st.dataframe(summary_df, width="stretch")
diff --git a/src/entropice/dashboard/views/inference_page.py b/src/entropice/dashboard/views/inference_page.py
index 0ddd6b3..83ee95b 100644
--- a/src/entropice/dashboard/views/inference_page.py
+++ b/src/entropice/dashboard/views/inference_page.py
@@ -85,7 +85,7 @@ def render_inference_statistics_section(predictions_gdf: gpd.GeoDataFrame, task:
Args:
predictions_gdf: GeoDataFrame with predictions.
- task: Task type ('binary', 'count', 'density').
+ task: Task type ('binary', 'count_regimes', 'density_regimes', 'count', 'density').
"""
st.header("📊 Inference Summary")
@@ -125,7 +125,7 @@ def render_class_distribution_section(predictions_gdf: gpd.GeoDataFrame, task: s
Args:
predictions_gdf: GeoDataFrame with predictions.
- task: Task type ('binary', 'count', 'density').
+ task: Task type ('binary', 'count_regimes', 'density_regimes', 'count', 'density').
"""
st.header("📈 Class Distribution")
@@ -138,7 +138,7 @@ def render_class_comparison_section(predictions_gdf: gpd.GeoDataFrame, task: str
Args:
predictions_gdf: GeoDataFrame with predictions.
- task: Task type ('binary', 'count', 'density').
+ task: Task type ('binary', 'count_regimes', 'density_regimes', 'count', 'density').
"""
st.header("🔍 Class Comparison Analysis")
diff --git a/src/entropice/experiments/feature_importance.py b/src/entropice/experiments/feature_importance.py
new file mode 100644
index 0000000..1a47856
--- /dev/null
+++ b/src/entropice/experiments/feature_importance.py
@@ -0,0 +1,87 @@
+from typing import cast
+
+import cyclopts
+from stopuhr import stopwatch
+
+from entropice.ml.autogluon import RunSettings as AutoGluonRunSettings
+from entropice.ml.autogluon import train as train_autogluon
+from entropice.ml.dataset import DatasetEnsemble
+from entropice.ml.hpsearchcv import RunSettings as HPOCVRunSettings
+from entropice.ml.hpsearchcv import hpsearch_cv
+from entropice.utils.paths import RESULTS_DIR
+from entropice.utils.types import Grid, Model, TargetDataset, Task
+
+cli = cyclopts.App("entropice-feature-importance")
+
+EXPERIMENT_NAME = "feature_importance_era5-shoulder_arcticdem"
+# EXPERIMENT_NAME = "tobis-final-tests"
+
+
+@cli.default
+def main(
+ grid: Grid,
+ target: TargetDataset,
+):
+ levels = [3, 4, 5, 6] if grid == "hex" else [6, 7, 8, 9, 10]
+ levels = [3, 6] if grid == "hex" else [6, 10]
+ for level in levels:
+ print(f"Running feature importance experiment for {grid} grid at level {level}...")
+ dimension_filters = {"ArcticDEM": {"aggregations": ["median"]}}
+ if (grid == "hex" and level in [3, 4]) or (grid == "healpix" and level in [6, 7]):
+ dimension_filters["ERA5-shoulder"] = {"aggregations": ["median"]}
+ dataset_ensemble = DatasetEnsemble(
+ grid=grid, level=level, members=["ArcticDEM", "ERA5-shoulder"], dimension_filters=dimension_filters
+ )
+
+ for task in cast(list[Task], ["binary", "density"]):
+ print(f"\nRunning for {task}...")
+
+ # AutoGluon
+ time_limit = 30 * 60 # 30 minutes
+ # time_limit = 60
+ presets = "extreme"
+ # presets = "medium"
+ settings = AutoGluonRunSettings(
+ time_limit=time_limit,
+ presets=presets,
+ verbosity=2,
+ task=task,
+ target=target,
+ )
+ train_autogluon(dataset_ensemble, settings, experiment=EXPERIMENT_NAME)
+
+ # HPOCV
+ splitter = "stratified_shuffle" if task == "binary" else "kfold"
+ models: list[Model] = ["xgboost", "rf", "knn"]
+ if task == "binary":
+ models.append("espa")
+ for model in models:
+ print(f"\nRunning HPOCV for model {model}...")
+ n_iter = {
+ "espa": 300,
+ "xgboost": 100,
+ "rf": 40,
+ "knn": 20,
+ }[model]
+ # n_iter = 3
+ scaler = "standard" if model in ["espa", "knn"] else "none"
+ normalize = scaler != "none"
+ settings = HPOCVRunSettings(
+ n_iter=n_iter,
+ task=task,
+ target=target,
+ splitter=splitter,
+ model=model,
+ scaler=scaler,
+ normalize=normalize,
+ )
+ hpsearch_cv(dataset_ensemble, settings, experiment=EXPERIMENT_NAME)
+
+ stopwatch.summary()
+ times = stopwatch.export()
+ times.to_parquet(RESULTS_DIR / EXPERIMENT_NAME / f"training_times_{target}_{grid}.parquet")
+ print("Done.")
+
+
+if __name__ == "__main__":
+ cli()
diff --git a/src/entropice/ingest/darts.py b/src/entropice/ingest/darts.py
index 460dde0..f7d973f 100644
--- a/src/entropice/ingest/darts.py
+++ b/src/entropice/ingest/darts.py
@@ -27,6 +27,7 @@ pretty.install()
darts_v1_l2_file = DARTS_V1_DIR / "DARTS_NitzeEtAl_v1-2_features_2018-2023_level2.parquet"
darts_v1_l2_cov_file = DARTS_V1_DIR / "DARTS_NitzeEtAl_v1-2_coverage_2018-2023_level2.parquet"
+darts_v1_corrections = DARTS_V1_DIR / "negative_correction.geojson"
darts_ml_training_labels_repo = DARTS_MLLABELS_DIR / "ML_training_labels" / "retrogressive_thaw_slumps"
cli = cyclopts.App(name="darts-rts")
@@ -153,6 +154,22 @@ def extract_darts_v1(grid: Grid, level: int):
darts_l2 = gpd.read_parquet(darts_v1_l2_file)
darts_cov_l2 = gpd.read_parquet(darts_v1_l2_cov_file)
grid_gdf, cell_areas = _load_grid(grid, level)
+ corrections = gpd.read_file(darts_v1_corrections).to_crs(darts_l2.crs)
+
+ with stopwatch("Apply corrections"):
+ # The correction file is just an area of sure negatives
+ # Thus, we first need to remove all RTS labels that intersect with the correction area,
+ darts_l2 = gpd.overlay(darts_l2, corrections, how="difference")
+ # then we need to add the correction area as coverage to the coverage file per year.
+ darts_cov_l2_cor = []
+ for year in darts_cov_l2["year"].unique():
+ year_cov = darts_cov_l2[darts_cov_l2["year"] == year]
+ year_cov_cor = gpd.overlay(year_cov, corrections, how="union")
+ year_cov_cor["year"] = year
+ darts_cov_l2_cor.append(year_cov_cor)
+ darts_cov_l2_cor = gpd.GeoDataFrame(pd.concat(darts_cov_l2_cor, ignore_index=True))
+ darts_cov_l2_cor["year"] = darts_cov_l2_cor["year"].astype(int)
+ darts_cov_l2 = darts_cov_l2_cor
with stopwatch("Assign RTS to grid"):
grid_l2 = grid_gdf.overlay(darts_l2.to_crs(grid_gdf.crs), how="intersection")
diff --git a/src/entropice/ml/__init__.py b/src/entropice/ml/__init__.py
index d16455c..15a46f2 100644
--- a/src/entropice/ml/__init__.py
+++ b/src/entropice/ml/__init__.py
@@ -7,6 +7,6 @@ This package contains modules for machine learning workflows:
- inference: Batch prediction pipeline for trained classifiers
"""
-from . import dataset, inference, training
+from . import dataset, inference, randomsearch
-__all__ = ["dataset", "inference", "training"]
+__all__ = ["dataset", "inference", "randomsearch"]
diff --git a/src/entropice/ml/autogluon.py b/src/entropice/ml/autogluon.py
new file mode 100644
index 0000000..2ea10c0
--- /dev/null
+++ b/src/entropice/ml/autogluon.py
@@ -0,0 +1,191 @@
+"""Training of models with AutoGluon."""
+
+from dataclasses import dataclass
+from typing import cast
+
+import cyclopts
+import pandas as pd
+import shap.maskers
+import xarray as xr
+from autogluon.tabular import TabularDataset, TabularPredictor
+from rich import pretty, traceback
+from shap import Explainer, Explanation
+from sklearn import set_config
+from stopuhr import stopwatch
+
+from entropice.ml.dataset import DatasetEnsemble
+from entropice.ml.inference import predict_proba
+from entropice.utils.paths import get_training_results_dir
+from entropice.utils.training import AutoML, Training
+from entropice.utils.types import TargetDataset, Task
+
+traceback.install()
+pretty.install()
+
+
+cli = cyclopts.App(
+ "entropice-autogluon",
+ config=cyclopts.config.Toml("autogluon-config.toml", root_keys=["tool", "entropice-autogluon"]), # ty:ignore[invalid-argument-type]
+)
+
+
+@cyclopts.Parameter("*")
+@dataclass(frozen=True, kw_only=True)
+class RunSettings:
+ """Run settings for training."""
+
+ time_limit: int = 3600 # Time limit in seconds (1 hour default)
+ presets: str = "extreme"
+ task: Task = "binary"
+ target: TargetDataset = "darts_v1"
+ verbosity: int = 2 # Verbosity level (0-4)
+
+
+def _compute_metrics_and_confusion_matrix( # noqa: C901
+ predictor: TabularPredictor, train_data: pd.DataFrame, test_data: pd.DataFrame, complete_data: pd.DataFrame
+) -> tuple[pd.DataFrame, xr.Dataset | None]:
+ train_scores = predictor.evaluate(train_data, display=True, detailed_report=True)
+ test_scores = predictor.evaluate(test_data, display=True, detailed_report=True)
+ complete_scores = predictor.evaluate(complete_data, display=True, detailed_report=True)
+ m = []
+ cm = {}
+ for dataset, scores in zip(["train", "test", "complete"], [train_scores, test_scores, complete_scores]):
+ for metric, score in scores.items():
+ if metric == "confusion_matrix":
+ score = cast(pd.DataFrame, score)
+ confusion_matrix = xr.DataArray(
+ score.to_numpy(),
+ dims=("y_true", "y_pred"),
+ coords={"y_true": score.index.tolist(), "y_pred": score.columns.tolist()},
+ )
+ cm[dataset] = confusion_matrix
+ elif metric == "classification_report":
+ score = cast(dict[str, dict[str, float]], score)
+ score.pop("accuracy") # Accuracy is already included as a separate metric
+ macro_avg = score.pop("macro avg")
+ for macro_avg_metric, macro_avg_score in macro_avg.items():
+ metric_name = f"macro_avg_{macro_avg_metric}"
+ m.append({"dataset": dataset, "metric": metric_name, "score": macro_avg_score})
+ weighted_avg = score.pop("weighted avg")
+ for weighted_avg_metric, weighted_avg_score in weighted_avg.items():
+ metric_name = f"weighted_avg_{weighted_avg_metric}"
+ m.append({"dataset": dataset, "metric": metric_name, "score": weighted_avg_score})
+ for class_name, class_scores in score.items():
+ class_name = class_name.replace(" ", "-")
+ for class_metric, class_score in class_scores.items():
+ m.append({"dataset": dataset, "metric": f"{class_name}_{class_metric}", "score": class_score})
+ else: # Scalar metric
+ m.append({"dataset": dataset, "metric": metric, "score": score})
+ if len(cm) == 0:
+ return pd.DataFrame(m), None
+ elif len(cm) == 3:
+ return pd.DataFrame(m), xr.Dataset(cm)
+ else:
+ raise RuntimeError("Confusion matrix should be computed for all datasets or none.")
+
+
+def _compute_shap_explanation(
+ predictor: TabularPredictor,
+ train_data: pd.DataFrame,
+ test_data: pd.DataFrame,
+ feature_names: list[str],
+ target_labels: list[str] | None,
+) -> Explanation:
+ masker = shap.maskers.Independent(data=train_data.drop(columns=["label"]))
+ explainer = Explainer(
+ predictor.predict_proba if predictor.problem_type in ["binary", "multiclass"] else predictor.predict,
+ masker=masker,
+ seed=42,
+ feature_names=feature_names,
+ output_names=target_labels,
+ )
+ samples = test_data.drop(columns=["label"])
+ if len(samples) > 200:
+ samples = samples.sample(n=200, random_state=42)
+ explanation = explainer(samples)
+ return explanation
+
+
+@cli.default
+def train(
+ dataset_ensemble: DatasetEnsemble,
+ settings: RunSettings = RunSettings(),
+ experiment: str | None = None,
+) -> Training:
+ """Perform random cross-validation on the training dataset.
+
+ Args:
+ dataset_ensemble (DatasetEnsemble): The dataset ensemble configuration.
+ settings (RunSettings): This runs settings.
+ experiment (str | None): Optional experiment name for results directory.
+
+ """
+ set_config(array_api_dispatch=False)
+ results_dir = get_training_results_dir(
+ experiment=experiment,
+ name="autogluon",
+ grid=dataset_ensemble.grid,
+ level=dataset_ensemble.level,
+ task=settings.task,
+ target=settings.target,
+ )
+ print(f"\n💾 Results directory: {results_dir}")
+
+ print("Creating training data...")
+ training_data = dataset_ensemble.create_training_set(task=settings.task, target=settings.target)
+ # Convert to AutoGluon TabularDataset
+ train_data: pd.DataFrame = TabularDataset(training_data.to_dataframe("train")) # ty:ignore[invalid-assignment]
+ test_data: pd.DataFrame = TabularDataset(training_data.to_dataframe("test")) # ty:ignore[invalid-assignment]
+ complete_data: pd.DataFrame = TabularDataset(training_data.to_dataframe(None)) # ty:ignore[invalid-assignment]
+
+ # Initialize TabularPredictor
+ print(f"\n🚀 Initializing AutoGluon TabularPredictor (preset='{settings.presets}')...")
+ predictor = TabularPredictor(
+ label="label",
+ path=str(results_dir / "models"),
+ verbosity=settings.verbosity,
+ )
+
+ # Train models
+ print(f"\n⚡ Training models for {settings.time_limit / 60}min...")
+ with stopwatch("AutoGluon training"):
+ predictor.fit(
+ train_data=train_data,
+ time_limit=settings.time_limit,
+ presets=settings.presets,
+ num_gpus=1,
+ )
+
+ print("\n📊 Evaluating model performance...")
+ leaderboard = predictor.leaderboard(test_data)
+ feature_importance = predictor.feature_importance(test_data)
+ metrics, confusion_matrix = _compute_metrics_and_confusion_matrix(predictor, train_data, test_data, complete_data)
+
+ with stopwatch("Explaining model predictions with SHAP..."):
+ explanation = _compute_shap_explanation(
+ predictor, train_data, test_data, training_data.feature_names, training_data.target_labels
+ )
+
+ print("Predicting probabilities for all cells...")
+ preds = predict_proba(dataset_ensemble, model=predictor, task=settings.task)
+ print(f"Predicted probabilities DataFrame with {len(preds)} entries.")
+
+ summary = Training(
+ path=results_dir,
+ dataset=dataset_ensemble,
+ method=AutoML(time_budget=settings.time_limit, preset=settings.presets, hpo=False),
+ task=settings.task,
+ target=settings.target,
+ training_set=training_data,
+ model=predictor,
+ model_type="autogluon",
+ metrics=metrics,
+ feature_importance=feature_importance,
+ shap_explanation=explanation,
+ predictions=preds,
+ confusion_matrix=confusion_matrix,
+ cv_results=None,
+ leaderboard=leaderboard,
+ )
+ summary.save()
+ return summary
diff --git a/src/entropice/ml/autogluon_training.py b/src/entropice/ml/autogluon_training.py
index ed33a67..939992f 100644
--- a/src/entropice/ml/autogluon_training.py
+++ b/src/entropice/ml/autogluon_training.py
@@ -1,4 +1,4 @@
-"""Training with AutoGluon TabularPredictor for automated ML."""
+"""DePRECATED!!! Training with AutoGluon TabularPredictor for automated ML."""
import pickle
from dataclasses import asdict, dataclass
@@ -12,7 +12,7 @@ from sklearn import set_config
from stopuhr import stopwatch
from entropice.ml.dataset import DatasetEnsemble
-from entropice.utils.paths import get_autogluon_results_dir
+from entropice.utils.paths import get_training_results_dir
from entropice.utils.types import TargetDataset, Task
traceback.install()
@@ -101,11 +101,13 @@ def autogluon_train(
print(f"📈 Evaluation metric: {eval_metric}")
# Create results directory
- results_dir = get_autogluon_results_dir(
+ results_dir = get_training_results_dir(
experiment=experiment,
grid=dataset_ensemble.grid,
level=dataset_ensemble.level,
task=settings.task,
+ target=settings.target,
+ name="autogluon",
)
print(f"\n💾 Results directory: {results_dir}")
diff --git a/src/entropice/ml/dataset.py b/src/entropice/ml/dataset.py
index 5e172df..d6b547e 100644
--- a/src/entropice/ml/dataset.py
+++ b/src/entropice/ml/dataset.py
@@ -18,7 +18,7 @@ from collections.abc import Generator
from dataclasses import asdict, dataclass, field
from functools import cache, cached_property
from itertools import product
-from typing import Literal, cast
+from typing import Literal, TypeVar, cast
import cupy as cp
import cyclopts
@@ -28,6 +28,7 @@ import pandas as pd
import seaborn as sns
import torch
import xarray as xr
+from cuml import KMeans
from rich import pretty, traceback
from sklearn import set_config
from sklearn.model_selection import train_test_split
@@ -118,26 +119,29 @@ def bin_values(
return binned
+ArrayType = TypeVar("ArrayType", torch.Tensor, np.ndarray, cp.ndarray)
+
+
@dataclass(frozen=True, eq=False)
-class SplittedArrays:
+class SplittedArrays[ArrayType: (torch.Tensor, np.ndarray, cp.ndarray)]:
"""Small wrapper for train and test arrays."""
- train: torch.Tensor | np.ndarray | cp.ndarray
- test: torch.Tensor | np.ndarray | cp.ndarray
+ train: ArrayType
+ test: ArrayType
@cached_property
- def combined(self) -> torch.Tensor | np.ndarray | cp.ndarray:
+ def combined(self) -> ArrayType:
"""Combined train and test arrays."""
if isinstance(self.train, torch.Tensor) and isinstance(self.test, torch.Tensor):
- return torch.cat([self.train, self.test], dim=0)
+ return torch.cat([self.train, self.test], dim=0) # ty:ignore[invalid-return-type]
elif isinstance(self.train, cp.ndarray) and isinstance(self.test, cp.ndarray):
return cp.concatenate([self.train, self.test], axis=0)
elif isinstance(self.train, np.ndarray) and isinstance(self.test, np.ndarray):
- return np.concatenate([self.train, self.test], axis=0)
+ return np.concatenate([self.train, self.test], axis=0) # ty:ignore[invalid-return-type]
else:
raise TypeError("Incompatible types for train and test arrays.")
- def as_numpy(self) -> "SplittedArrays":
+ def as_numpy(self) -> "SplittedArrays[np.ndarray]":
"""Return the arrays as numpy arrays."""
train_np = (
self.train.cpu().numpy()
@@ -157,13 +161,13 @@ class SplittedArrays:
@dataclass(frozen=True, eq=False)
-class TrainingSet:
+class TrainingSet[ArrayType: (torch.Tensor, np.ndarray, cp.ndarray)]:
"""Container for the training dataset."""
targets: gpd.GeoDataFrame
features: pd.DataFrame
- X: SplittedArrays
- y: SplittedArrays
+ X: SplittedArrays[ArrayType]
+ y: SplittedArrays[ArrayType]
z: pd.Series
split: pd.Series
@@ -214,6 +218,16 @@ class TrainingSet:
"""Names of the features."""
return self.features.columns.tolist()
+ @cached_property
+ def clusters(self) -> pd.Series:
+ """Geo-Cluster assignments for each sample, based on the geometries of the target GeoDataFrame."""
+ centroids = self.targets.to_crs("EPSG:3413")["geometry"].centroid
+ centroids = cp.array([centroids.x.to_numpy(), centroids.y.to_numpy()]).T
+ # Use kMeans to cluster the centroids into 10 clusters
+ kmeans = KMeans(n_clusters=10, random_state=42)
+ clusters = kmeans.fit_predict(centroids).get()
+ return pd.Series(clusters, index=self.targets.index)
+
def __len__(self):
return len(self.z)
diff --git a/src/entropice/ml/explainations.py b/src/entropice/ml/explainations.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/entropice/ml/hpsearchcv.py b/src/entropice/ml/hpsearchcv.py
new file mode 100644
index 0000000..0572342
--- /dev/null
+++ b/src/entropice/ml/hpsearchcv.py
@@ -0,0 +1,395 @@
+"""Training of models with hyperparameter search with cross-validation."""
+
+from dataclasses import dataclass
+from typing import Literal, cast
+
+import cupy as cp
+import cyclopts
+import numpy as np
+import pandas as pd
+import shap.maskers
+import torch
+import xarray as xr
+from rich import pretty, traceback
+from shap import Explainer, Explanation, TreeExplainer
+from sklearn import set_config
+from sklearn.inspection import permutation_importance
+from sklearn.metrics import confusion_matrix
+from sklearn.model_selection import RandomizedSearchCV
+from sklearn.pipeline import Pipeline
+from sklearn.preprocessing import (
+ FunctionTransformer,
+ MaxAbsScaler,
+ MinMaxScaler,
+ Normalizer,
+ QuantileTransformer,
+ StandardScaler,
+)
+from stopuhr import stopwatch
+
+from entropice.ml.dataset import DatasetEnsemble, SplittedArrays, TrainingSet
+from entropice.ml.inference import predict_proba
+from entropice.ml.models import (
+ ModelHPOConfig,
+ extract_espa_feature_importance,
+ extract_rf_feature_importance,
+ extract_xgboost_feature_importance,
+ get_model_hpo_config,
+ get_splitter,
+)
+from entropice.utils.metrics import get_metrics, metric_functions
+from entropice.utils.paths import get_training_results_dir
+from entropice.utils.training import HPOCV, Training, move_data_to_device
+from entropice.utils.types import HPSearch, Model, Scaler, Splitter, TargetDataset, Task
+
+traceback.install()
+pretty.install()
+
+
+cli = cyclopts.App(
+ "entropice-hpsearchcv",
+ config=cyclopts.config.Toml("hpsearchcv-config.toml", root_keys=["tool", "entropice-hpsearchcv"]), # ty:ignore[invalid-argument-type]
+)
+
+
+@cyclopts.Parameter("*")
+@dataclass(frozen=True, kw_only=True)
+class RunSettings:
+ """Run settings for training."""
+
+ n_iter: int = 2000
+ task: Task = "binary"
+ target: TargetDataset = "darts_v1"
+ search: HPSearch = "random" # TODO: Implement grid and bayesian search
+ splitter: Splitter = "stratified_shuffle"
+ model: Model = "espa"
+ scaler: Scaler = "standard"
+ normalize: bool = False
+
+ @property
+ def device(self) -> Literal["torch", "cuda", "cpu"]:
+ """Get the device to use for model training.
+
+ Note: Device Management currently is super nasty:
+ - CuML (RF & kNN) expects cupy ("cuda") arrays
+ - XGBoost always returns CPU arrays
+ - eSPA for some reason is super slow on cupy but very fast on torch tensors
+ - The sklearn permutation stuff does not work with torch tensors, only cupy and numpy arrays
+ - SHAP in general only works with CPU
+ """
+ return "torch" if self.model == "espa" else "cuda"
+
+ def build_pipeline(self, model_hpo_config: ModelHPOConfig) -> Pipeline: # noqa: C901
+ """Build a scikit-learn Pipeline based on the settings."""
+ # Add a feature scaler / normalization step if specified, but assert that it's only used for non-Tree models
+ if self.model in ["rf", "xgboost"]:
+ assert self.scaler == "none", f"Scaler {self.scaler} is not viable with model {self.model}"
+ elif self.scaler == "none":
+ assert self.scaler != "none", f"No scaler specified for model {self.model}, which is not viable."
+
+ match self.scaler:
+ case "standard":
+ scaler = StandardScaler()
+ case "minmax":
+ scaler = MinMaxScaler()
+ case "maxabs":
+ scaler = MaxAbsScaler()
+ case "quantile":
+ scaler = QuantileTransformer(output_distribution="normal")
+ case "none":
+ scaler = None
+ case _:
+ raise ValueError(f"Unknown scaler: {self.scaler}")
+
+ pipeline_steps = []
+ if scaler is not None:
+ print(f"Using {scaler.__class__.__name__} for feature scaling.")
+ pipeline_steps.append(("scaler", scaler))
+ if self.normalize:
+ print("Using Normalizer for feature normalization.")
+ pipeline_steps.append(("normalizer", Normalizer()))
+ # Necessary, because scaler and normalizer are only GPU ready in 1.8.0 but autogluon requries <1.8.0
+ if len(pipeline_steps) > 0 and self.device != "cpu":
+ print(f"Adding steps to move data to CPU for preprocessing and back to {self.device} for model fitting.")
+ to_cpu = FunctionTransformer(move_data_to_device, kw_args={"device": "cpu"})
+ pipeline_steps.insert(0, ("to_cpu", to_cpu))
+ to_gpu = FunctionTransformer(move_data_to_device, kw_args={"device": self.device})
+ pipeline_steps.append(("to_device", to_gpu))
+ pipeline_steps.append(("model", model_hpo_config.model))
+ return Pipeline(pipeline_steps)
+
+ def build_search(
+ self, pipeline: Pipeline, model_hpo_config: ModelHPOConfig, metrics: list[str], refit: str
+ ) -> RandomizedSearchCV:
+ """Build a scikit-learn RandomizedSearchCV based on the settings."""
+ if self.task in ["density", "count"]:
+ assert self.splitter not in ["stratified_shuffle", "stratified_kfold"], (
+ f"Splitter {self.splitter} is not viable for regression tasks"
+ )
+
+ cv = get_splitter(self.splitter, n_splits=5)
+ print(f"Using {cv.__class__.__name__} for cross-validation splitting.")
+
+ search_space = {f"model__{k}": v for k, v in model_hpo_config.search_space.items()}
+ if self.search == "random":
+ hp_search = RandomizedSearchCV(
+ estimator=pipeline,
+ param_distributions=search_space,
+ n_iter=self.n_iter,
+ cv=cv,
+ scoring=metrics,
+ refit=refit,
+ random_state=42,
+ verbose=2,
+ )
+ else:
+ raise NotImplementedError(f"Search method {self.search} not implemented yet.")
+ return hp_search
+
+
+def _extract_cv_results(cv_results) -> pd.DataFrame:
+ cv_results = pd.DataFrame(cv_results)
+ # Parse the params into individual columns
+ params = pd.json_normalize(cv_results["params"]) # ty:ignore[invalid-argument-type]
+ cv_results = pd.concat([cv_results.drop(columns=["params"]), params], axis=1)
+ return cv_results
+
+
+def _compute_metrics(y: SplittedArrays, y_pred: SplittedArrays, metrics: list[str]) -> pd.DataFrame:
+ m = []
+ for metric in metrics:
+ metric_fn = metric_functions[metric]
+ for split in ["train", "test", "combined"]:
+ value = metric_fn(getattr(y, split), getattr(y_pred, split))
+ m.append({"metric": metric, "split": split, "value": value})
+ return pd.DataFrame(m)
+
+
+def _compute_confusion_matrices(
+ y: SplittedArrays, y_pred: SplittedArrays, codes: np.ndarray, labels: list[str] | None
+) -> xr.Dataset:
+ if labels is None:
+ labels = [str(code) for code in codes]
+ cm = xr.Dataset(
+ {
+ "test": (("true_label", "predicted_label"), confusion_matrix(y.test, y_pred.test, labels=codes)),
+ "train": (("true_label", "predicted_label"), confusion_matrix(y.train, y_pred.train, labels=codes)),
+ "combined": (
+ ("true_label", "predicted_label"),
+ confusion_matrix(y.combined, y_pred.combined, labels=codes),
+ ),
+ },
+ coords={"true_label": labels, "predicted_label": labels},
+ )
+ return cm
+
+
+def _compute_feature_importance(model: Model, best_estimator: Pipeline, training_data: TrainingSet) -> pd.DataFrame:
+ X_test = training_data.X.as_numpy().test if model in ["xgboost", "espa"] else training_data.X.test # noqa: N806
+ y_test = training_data.y.as_numpy().test if model in ["xgboost", "espa"] else training_data.y.test
+ if model == "espa":
+ # eSPA is super slow with cupy arrays, so we need to move the data to CPU for permutation importance
+ # To to this, we manually recreate the pipeline but without the device steps
+ if "scaler" in best_estimator.named_steps:
+ X_test = best_estimator.named_steps["scaler"].transform(X_test) # noqa: N806
+ if "normalizer" in best_estimator.named_steps:
+ X_test = best_estimator.named_steps["normalizer"].transform(X_test) # noqa: N806
+ best_estimator.named_steps["model"].to_numpy() # inplace
+
+ r = permutation_importance(
+ best_estimator if model != "espa" else best_estimator.named_steps["model"],
+ X_test,
+ y_test,
+ n_repeats=5,
+ random_state=0,
+ max_samples=min(5000, training_data.X.test.shape[0]),
+ )
+
+ if model == "espa":
+ best_estimator.named_steps["model"].to_torch() # inplace
+
+ feature_importances = pd.DataFrame(
+ {
+ "importance": r["importances_mean"],
+ "stddev": r["importances_std"],
+ },
+ index=training_data.feature_names,
+ )
+ match model:
+ case "espa":
+ model_feature_importances = extract_espa_feature_importance(
+ best_estimator.named_steps["model"], training_data
+ ).rename(columns={"importance": "model_feature_weights"})
+ case "xgboost":
+ model_feature_importances = extract_xgboost_feature_importance(
+ best_estimator.named_steps["model"], training_data
+ )
+ case "rf":
+ model_feature_importances = extract_rf_feature_importance(
+ best_estimator.named_steps["model"], training_data
+ )
+ case _:
+ model_feature_importances = None
+ if model_feature_importances is not None:
+ feature_importances = feature_importances.join(model_feature_importances)
+ return feature_importances
+
+
+def _compute_shap_explanation(model: Model, best_estimator: Pipeline, training_data: TrainingSet) -> Explanation:
+ match model:
+ case "espa" | "knn" | "rf": # CUML models do not yet work with TreeExplainer...
+ train_transformed = training_data.X.as_numpy().train
+ if "scaler" in best_estimator.named_steps:
+ train_transformed = best_estimator.named_steps["scaler"].transform(train_transformed)
+ if "normalizer" in best_estimator.named_steps:
+ train_transformed = best_estimator.named_steps["normalizer"].transform(train_transformed)
+ masker = shap.maskers.Independent(data=train_transformed)
+
+ def _model_predict(data):
+ mdl = best_estimator.named_steps["model"]
+ f = mdl.predict_proba if hasattr(mdl, "predict_proba") else mdl.predict
+
+ out = f(data)
+ if isinstance(out, torch.Tensor):
+ out = out.cpu().numpy()
+ elif isinstance(out, cp.ndarray):
+ out = out.get()
+ return out
+
+ explainer = Explainer(
+ _model_predict,
+ masker=masker,
+ seed=42,
+ feature_names=training_data.feature_names,
+ output_names=training_data.target_labels,
+ )
+ case "xgboost":
+ explainer = TreeExplainer(best_estimator.named_steps["model"], feature_names=training_data.feature_names)
+ case _:
+ raise ValueError(f"Unknown model: {model}")
+
+ samples = training_data.X.as_numpy().test
+ if len(samples) > 200:
+ rng = np.random.default_rng(seed=42)
+ sample_indices = rng.choice(len(samples), size=200, replace=False)
+ samples = samples[sample_indices]
+ if "scaler" in best_estimator.named_steps:
+ samples = best_estimator.named_steps["scaler"].transform(samples)
+ if "normalizer" in best_estimator.named_steps:
+ samples = best_estimator.named_steps["normalizer"].transform(samples)
+ explanation = explainer(samples)
+ return explanation
+
+
+@cli.default
+def hpsearch_cv(
+ dataset_ensemble: DatasetEnsemble,
+ settings: RunSettings = RunSettings(),
+ experiment: str | None = None,
+) -> Training:
+ """Perform random cross-validation on the training dataset.
+
+ Args:
+ dataset_ensemble (DatasetEnsemble): The dataset ensemble configuration.
+ settings (RunSettings): This runs settings.
+ experiment (str | None): Optional experiment name for results directory.
+
+ """
+ results_dir = get_training_results_dir(
+ experiment=experiment,
+ name="random_search",
+ grid=dataset_ensemble.grid,
+ level=dataset_ensemble.level,
+ task=settings.task,
+ target=settings.target,
+ model_type=settings.model,
+ )
+
+ # Since we use cuml and xgboost libraries, we can only enable array API for ESPA
+ use_array_api = settings.model != "xgboost"
+ set_config(array_api_dispatch=use_array_api)
+
+ print("Creating training data...")
+ training_data = dataset_ensemble.create_training_set(
+ task=settings.task, target=settings.target, device=settings.device
+ )
+
+ model_hpo_config = get_model_hpo_config(settings.model, settings.task)
+ print(f"Using model: {settings.model} with parameters: {model_hpo_config.hp_config}")
+
+ metrics, refit = get_metrics(settings.task)
+ print(f"Using {len(metrics)} metrics as scoring and {refit} for refitting.")
+
+ pipeline = settings.build_pipeline(model_hpo_config)
+ print(f"Pipeline steps: {pipeline.named_steps}")
+
+ hp_search = settings.build_search(pipeline, model_hpo_config, metrics, refit)
+ print(f"Starting hyperparameter search with {settings.n_iter} iterations...")
+ with stopwatch(f"RandomizedSearchCV fitting for {settings.n_iter} candidates"):
+ fit_params = {f"model__{k}": v for k, v in model_hpo_config.fit_params.items()}
+ hp_search.fit(
+ training_data.X.train,
+ # XGBoost returns it's labels as numpy arrays instead of cupy arrays
+ # Thus, for the scoring to work, we need to convert them back to numpy
+ training_data.y.as_numpy().train if settings.model == "xgboost" else training_data.y.train,
+ **fit_params,
+ )
+ print("Best parameters combination found:")
+ best_estimator = cast(Pipeline, hp_search.best_estimator_)
+ best_parameters = best_estimator.get_params()
+ for param_name in sorted(model_hpo_config.hp_config.keys()):
+ search_param_name = f"model__{param_name}"
+ print(f"{param_name}: {best_parameters[search_param_name]}")
+
+ # Compute predictions on the all sets and move them to numpy for metric computations
+ y_pred = SplittedArrays(
+ train=best_estimator.predict(training_data.X.train),
+ test=best_estimator.predict(training_data.X.test),
+ ).as_numpy()
+ y = training_data.y.as_numpy()
+ metrics = _compute_metrics(y, y_pred, metrics)
+ if settings.task in ["binary", "count_regimes", "density_regimes"]:
+ confusion_matrix = _compute_confusion_matrices(
+ y, y_pred, codes=np.array(training_data.target_codes), labels=training_data.target_labels
+ )
+ else:
+ confusion_matrix = None
+ print("Metrics computed!")
+
+ with stopwatch("Computing feature importance with permutation importance..."):
+ feature_importance = _compute_feature_importance(settings.model, best_estimator, training_data)
+
+ with stopwatch("Explaining model predictions with SHAP..."):
+ explanation = _compute_shap_explanation(settings.model, best_estimator, training_data)
+
+ print("Predicting probabilities for all cells...")
+ preds = predict_proba(dataset_ensemble, model=best_estimator, task=settings.task, device=settings.device)
+ print(f"Predicted probabilities DataFrame with {len(preds)} entries.")
+
+ summary = Training(
+ path=results_dir,
+ dataset=dataset_ensemble,
+ method=HPOCV(
+ method=settings.search,
+ splitter=settings.splitter,
+ scaler=settings.scaler,
+ normalize=settings.normalize,
+ n_iter=settings.n_iter,
+ hpconfig=model_hpo_config.hp_config,
+ ),
+ task=settings.task,
+ target=settings.target,
+ training_set=training_data,
+ model=best_estimator,
+ model_type=settings.model,
+ metrics=metrics,
+ feature_importance=feature_importance,
+ shap_explanation=explanation,
+ predictions=preds,
+ confusion_matrix=confusion_matrix,
+ cv_results=_extract_cv_results(hp_search.cv_results_),
+ leaderboard=None,
+ )
+ summary.save()
+
+ return summary
diff --git a/src/entropice/ml/inference.py b/src/entropice/ml/inference.py
index adbd62e..09c1c21 100644
--- a/src/entropice/ml/inference.py
+++ b/src/entropice/ml/inference.py
@@ -5,13 +5,17 @@ from typing import Literal
import cupy as cp
import geopandas as gpd
+import numpy as np
import pandas as pd
import torch
+from autogluon.tabular import TabularPredictor
from rich import pretty, traceback
from sklearn import set_config
+from sklearn.pipeline import Pipeline
from entropice.ml.dataset import DatasetEnsemble
-from entropice.ml.models import SupportedModel
+from entropice.ml.models import SupportedModel, is_classifier, is_regressor
+from entropice.utils.types import Task
traceback.install()
pretty.install()
@@ -19,16 +23,53 @@ pretty.install()
set_config(array_api_dispatch=True)
+def _assert_right_model_for_task(model: Pipeline | SupportedModel | TabularPredictor, task: Task) -> None:
+ assert hasattr(model, "predict") or hasattr(model, "predict_proba"), (
+ "Model must have a predict or predict_proba method"
+ )
+ is_classification = task in ["binary", "count_regimes", "density_regimes"]
+ is_regression = task in ["count", "density"]
+ assert is_classification != is_regression, "Task must be either classification or regression"
+ assert is_classifier(model) == is_classification, f"Model type does not match task type for task {task}"
+ assert is_regressor(model) == is_regression, f"Model type does not match task type for task {task}"
+
+
+def _categorize_predictions(
+ preds: pd.Series | np.ndarray,
+ task: Task,
+) -> pd.Series | pd.Categorical:
+ """Convert the raw model predictions into a category type series."""
+ if isinstance(preds, np.ndarray):
+ preds = pd.Series(preds)
+ match task:
+ case "binary" | "count_regimes" | "density_regimes":
+ labels_dict = {
+ "binary": ["No RTS", "RTS"],
+ "count_regimes": ["None", "Very Few", "Few", "Several", "Many", "Very Many"],
+ "density_regimes": ["Empty", "Very Sparse", "Sparse", "Moderate", "Dense", "Very Dense"],
+ }
+ categories = pd.CategoricalDtype(categories=labels_dict[task], ordered=task != "binary")
+ # Check if preds are codes or labels
+ if preds.dtype == "object" or isinstance(preds.iloc[0], str):
+ return pd.Categorical(preds, dtype=categories)
+ else:
+ return pd.Categorical.from_codes(preds.astype(int).to_list(), dtype=categories)
+ case _:
+ return preds
+
+
def predict_proba(
e: DatasetEnsemble,
- model: SupportedModel,
+ model: Pipeline | SupportedModel | TabularPredictor,
+ task: Task,
device: Literal["cpu", "cuda", "torch"] = "cuda",
) -> gpd.GeoDataFrame:
"""Get predicted probabilities for each cell.
Args:
e (DatasetEnsemble): The dataset ensemble configuration.
- model: SupportedModel: The trained model to use for predictions.
+ model: SupportedModel | TabularPredictor: The trained model to use for predictions.
+ task (Task): The task.
device (Literal["cpu", "cuda", "torch"]): The device to use for predictions.
This must match with the state of the model!
@@ -36,6 +77,7 @@ def predict_proba(
gpd.GeoDataFrame: A GeoDataFrame with cell_id, predicted probability, and geometry.
"""
+ _assert_right_model_for_task(model, task)
# Predict in batches to avoid memory issues
batch_size = 50000
preds = []
@@ -52,16 +94,27 @@ def predict_proba(
cell_ids = batch.index.to_numpy()
cell_geoms = grid_gdf.loc[batch.index, "geometry"].to_numpy()
- X_batch = batch.to_numpy(dtype="float64")
- if device == "torch":
- X_batch = torch.from_numpy(X_batch).to("cuda")
- elif device == "cuda":
- X_batch = cp.asarray(X_batch)
- batch_preds = model.predict(X_batch)
- if isinstance(batch_preds, cp.ndarray):
- batch_preds = batch_preds.get()
- elif torch.is_tensor(batch_preds):
- batch_preds = batch_preds.cpu().numpy()
+ if isinstance(model, TabularPredictor):
+ print(f"Predicting batch of size {len(batch)} ({type(batch)}) with AutoGluon TabularPredictor...")
+ batch_preds = model.predict(batch)
+ print(f"Batch predictions type: {type(batch_preds)}, shape: {batch_preds.shape}")
+
+ assert isinstance(batch_preds, pd.DataFrame | pd.Series), (
+ "AutoGluon predict should return a DataFrame or Series"
+ )
+ batch_preds = batch_preds.to_numpy()
+ else:
+ X_batch = batch.to_numpy(dtype="float64")
+ if device == "torch":
+ X_batch = torch.from_numpy(X_batch).to("cuda")
+ elif device == "cuda":
+ X_batch = cp.asarray(X_batch)
+ batch_preds = model.predict(X_batch)
+ if isinstance(batch_preds, cp.ndarray):
+ batch_preds = batch_preds.get()
+ elif torch.is_tensor(batch_preds):
+ batch_preds = batch_preds.cpu().numpy()
+ batch_preds = _categorize_predictions(batch_preds, task=task)
batch_preds = gpd.GeoDataFrame(
{
"cell_id": cell_ids,
diff --git a/src/entropice/ml/models.py b/src/entropice/ml/models.py
index c94719f..4dd2492 100644
--- a/src/entropice/ml/models.py
+++ b/src/entropice/ml/models.py
@@ -3,16 +3,30 @@
from dataclasses import dataclass, field
from typing import TypedDict
+import cupy as cp
+import pandas as pd
import scipy.stats
+import torch
import xarray as xr
+from autogluon.tabular import TabularPredictor
from cuml.ensemble import RandomForestClassifier, RandomForestRegressor
from cuml.neighbors import KNeighborsClassifier, KNeighborsRegressor
from entropy import ESPAClassifier
from scipy.stats._distn_infrastructure import rv_continuous_frozen, rv_discrete_frozen
+from sklearn.model_selection import (
+ GroupKFold,
+ GroupShuffleSplit,
+ KFold,
+ ShuffleSplit,
+ StratifiedGroupKFold,
+ StratifiedKFold,
+ StratifiedShuffleSplit,
+)
+from sklearn.pipeline import Pipeline
from xgboost.sklearn import XGBClassifier, XGBRegressor
from entropice.ml.dataset import TrainingSet
-from entropice.utils.types import Task
+from entropice.utils.types import Splitter, Task
class Distribution(TypedDict):
@@ -35,6 +49,47 @@ type SupportedModel = (
)
+def get_search_space(hp_config: HPConfig) -> dict[str, list | rv_continuous_frozen | rv_discrete_frozen]:
+ """Convert the HPConfig into a search space dictionary usable by sklearn's RandomizedSearchCV."""
+ search_space = {}
+ for key, dist in hp_config.items():
+ if isinstance(dist, list):
+ search_space[key] = dist
+ continue
+ assert hasattr(scipy.stats, dist["distribution"]), (
+ f"Unknown distribution type for {key}: {dist['distribution']}"
+ )
+ distfn = getattr(scipy.stats, dist["distribution"])
+ search_space[key] = distfn(dist["low"], dist["high"])
+ return search_space
+
+
+def is_classifier(model: Pipeline | SupportedModel | TabularPredictor) -> bool:
+ """Check if the model is a classifier."""
+ if isinstance(model, TabularPredictor):
+ return model.problem_type in ["binary", "multiclass"]
+ if isinstance(model, Pipeline):
+ # Check the last step of the pipeline for the model type
+ return is_classifier(model.steps[-1][1])
+ return isinstance(
+ model,
+ (ESPAClassifier, XGBClassifier, RandomForestClassifier, KNeighborsClassifier),
+ )
+
+
+def is_regressor(model: Pipeline | SupportedModel | TabularPredictor) -> bool:
+ """Check if the model is a regressor."""
+ if isinstance(model, TabularPredictor):
+ return model.problem_type in ["regression"]
+ if isinstance(model, Pipeline):
+ # Check the last step of the pipeline for the model type
+ return is_regressor(model.steps[-1][1])
+ return isinstance(
+ model,
+ (XGBRegressor, RandomForestRegressor, KNeighborsRegressor),
+ )
+
+
@dataclass(frozen=True)
class ModelHPOConfig:
"""Model - Hyperparameter Optimization Configuration."""
@@ -46,32 +101,43 @@ class ModelHPOConfig:
@property
def search_space(self) -> dict[str, list | rv_continuous_frozen | rv_discrete_frozen]:
"""Convert the HPConfig into a search space dictionary usable by sklearn's RandomizedSearchCV."""
- search_space = {}
- for key, dist in self.hp_config.items():
- if isinstance(dist, list):
- search_space[key] = dist
- continue
- assert hasattr(scipy.stats, dist["distribution"]), (
- f"Unknown distribution type for {key}: {dist['distribution']}"
- )
- distfn = getattr(scipy.stats, dist["distribution"])
- search_space[key] = distfn(dist["low"], dist["high"])
- return search_space
+ return get_search_space(self.hp_config)
# Hardcode Search Settings for now
espa_hpconfig: HPConfig = {
"eps_cl": {"distribution": "loguniform", "low": 1e-11, "high": 1e-6},
"eps_e": {"distribution": "loguniform", "low": 1e4, "high": 1e8},
- "initial_K": {"distribution": "randint", "low": 400, "high": 800},
+ "initial_K": {"distribution": "randint", "low": 50, "high": 800},
}
xgboost_hpconfig: HPConfig = {
- "learning_rate": {"distribution": "loguniform", "low": 1e-3, "high": 1e-1},
- "n_estimators": {"distribution": "randint", "low": 50, "high": 2000},
+ # Learning & Regularization
+ "learning_rate": {"distribution": "loguniform", "low": 0.01, "high": 0.3},
+ "n_estimators": {"distribution": "randint", "low": 100, "high": 500},
+ # Tree Structure (critical for overfitting control)
+ "max_depth": {"distribution": "randint", "low": 3, "high": 10},
+ # "min_child_weight": {"distribution": "randint", "low": 1, "high": 10},
+ # Feature Sampling (important for high-dimensional data)
+ "colsample_bytree": {"distribution": "uniform", "low": 0.3, "high": 1.0},
+ # "colsample_bylevel": {"distribution": "uniform", "low": 0.3, "high": 1.0},
+ # Row Sampling
+ # "subsample": {"distribution": "uniform", "low": 0.5, "high": 1.0},
+ # Regularization
+ # "reg_alpha": {"distribution": "loguniform", "low": 1e-8, "high": 1.0}, # L1
+ "reg_lambda": {"distribution": "loguniform", "low": 1e-8, "high": 10.0}, # L2
}
rf_hpconfig: HPConfig = {
- "max_depth": {"distribution": "randint", "low": 5, "high": 50},
- "n_estimators": {"distribution": "randint", "low": 50, "high": 1000},
+ # Tree Structure
+ "max_depth": {"distribution": "randint", "low": 10, "high": 50},
+ "n_estimators": {"distribution": "randint", "low": 50, "high": 300},
+ # Split Criteria (critical for >100 features)
+ "max_features": {"distribution": "uniform", "low": 0.1, "high": 0.5}, # sqrt(100) ≈ 10% of features
+ "min_samples_split": {"distribution": "randint", "low": 2, "high": 20},
+ # "min_samples_leaf": {"distribution": "randint", "low": 1, "high": 10},
+ # Regularization (cuML specific)
+ # "min_impurity_decrease": {"distribution": "loguniform", "low": 1e-7, "high": 1e-2},
+ # Bootstrap
+ # "max_samples": {"distribution": "uniform", "low": 0.5, "high": 1.0},
}
knn_hpconfig: HPConfig = {
"n_neighbors": {"distribution": "randint", "low": 10, "high": 200},
@@ -134,7 +200,7 @@ def get_model_hpo_config(model: str, task: Task, **model_kwargs) -> ModelHPOConf
raise ValueError(f"Unsupported model/task combination: {model}/{task}")
-def extract_espa_feature_importance(model: ESPAClassifier, training_data: TrainingSet) -> xr.Dataset:
+def extract_espa_state(model: ESPAClassifier, training_data: TrainingSet) -> xr.Dataset:
"""Extract the inner state of a trained ESPAClassifier as an xarray Dataset."""
# Annotate the state with xarray metadata
boxes = list(range(model.K_))
@@ -157,7 +223,7 @@ def extract_espa_feature_importance(model: ESPAClassifier, training_data: Traini
dims=["feature"],
coords={"feature": training_data.feature_names},
name="feature_weights",
- attrs={"description": "Feature weights for each box."},
+ attrs={"description": "Weights for each feature."},
)
state = xr.Dataset(
{
@@ -172,130 +238,68 @@ def extract_espa_feature_importance(model: ESPAClassifier, training_data: Traini
return state
-def extract_xgboost_feature_importance(model: XGBClassifier | XGBRegressor, training_data: TrainingSet) -> xr.Dataset:
- """Extract feature importance from a trained XGBoost model as an xarray Dataset."""
- # Extract XGBoost-specific information
- # Get the underlying booster
+def extract_espa_feature_importance(model: ESPAClassifier, training_data: TrainingSet) -> pd.DataFrame:
+ """Extract feature importance from a trained ESPAClassifier as a pandas DataFrame."""
+ weights = model.W_
+ if isinstance(weights, torch.Tensor):
+ weights = weights.cpu().numpy()
+ elif isinstance(weights, cp.ndarray):
+ weights = weights.get()
+ return pd.DataFrame(
+ {"importance": weights},
+ index=training_data.feature_names,
+ )
+
+
+def extract_xgboost_feature_importance(model: XGBClassifier | XGBRegressor, training_data: TrainingSet) -> pd.DataFrame:
+ """Extract feature importance from a trained XGBoost model as a pandas DataFrame."""
booster = model.get_booster()
-
- # Feature importance with different importance types
- # Note: get_score() returns dict with keys like 'f0', 'f1', etc. (feature indices)
- importance_weight = booster.get_score(importance_type="weight")
- importance_gain = booster.get_score(importance_type="gain")
- importance_cover = booster.get_score(importance_type="cover")
- importance_total_gain = booster.get_score(importance_type="total_gain")
- importance_total_cover = booster.get_score(importance_type="total_cover")
-
- # Create aligned arrays for all features (including zero-importance)
- def align_importance(importance_dict, features):
- """Align importance dict to feature list, filling missing with 0.
-
- XGBoost returns feature indices (f0, f1, ...) as keys, so we need to map them.
- """
- return [importance_dict.get(f"f{i}", 0.0) for i in range(len(features))]
-
- feature_importance_weight = xr.DataArray(
- align_importance(importance_weight, training_data.feature_names),
- dims=["feature"],
- coords={"feature": training_data.feature_names},
- name="feature_importance_weight",
- attrs={"description": "Number of times a feature is used to split the data across all trees."},
- )
- feature_importance_gain = xr.DataArray(
- align_importance(importance_gain, training_data.feature_names),
- dims=["feature"],
- coords={"feature": training_data.feature_names},
- name="feature_importance_gain",
- attrs={"description": "Average gain across all splits the feature is used in."},
- )
- feature_importance_cover = xr.DataArray(
- align_importance(importance_cover, training_data.feature_names),
- dims=["feature"],
- coords={"feature": training_data.feature_names},
- name="feature_importance_cover",
- attrs={"description": "Average coverage across all splits the feature is used in."},
- )
- feature_importance_total_gain = xr.DataArray(
- align_importance(importance_total_gain, training_data.feature_names),
- dims=["feature"],
- coords={"feature": training_data.feature_names},
- name="feature_importance_total_gain",
- attrs={"description": "Total gain across all splits the feature is used in."},
- )
- feature_importance_total_cover = xr.DataArray(
- align_importance(importance_total_cover, training_data.feature_names),
- dims=["feature"],
- coords={"feature": training_data.feature_names},
- name="feature_importance_total_cover",
- attrs={"description": "Total coverage across all splits the feature is used in."},
- )
-
- # Store tree information
- n_trees = booster.num_boosted_rounds()
-
- state = xr.Dataset(
- {
- "feature_importance_weight": feature_importance_weight,
- "feature_importance_gain": feature_importance_gain,
- "feature_importance_cover": feature_importance_cover,
- "feature_importance_total_gain": feature_importance_total_gain,
- "feature_importance_total_cover": feature_importance_total_cover,
- },
- attrs={
- "description": "Inner state of the best XGBClassifier from RandomizedSearchCV.",
- "n_trees": n_trees,
- "objective": str(model.objective),
- },
- )
- return state
+ fi = {}
+ for metric in ["weight", "gain", "cover", "total_gain", "total_cover"]:
+ scores = booster.get_score(importance_type=metric)
+ fi[metric] = [scores.get(f"f{i}", 0.0) for i in range(len(training_data.feature_names))]
+ return pd.DataFrame(fi, index=training_data.feature_names)
def extract_rf_feature_importance(
model: RandomForestClassifier | RandomForestRegressor, training_data: TrainingSet
-) -> xr.Dataset:
- """Extract feature importance from a trained RandomForest model as an xarray Dataset."""
+) -> pd.DataFrame:
+ """Extract feature importance from a trained RandomForest model as a pandas DataFrame."""
# Extract Random Forest-specific information
# Note: cuML's RandomForestClassifier doesn't expose individual trees (estimators_)
# like sklearn does, so we can only extract feature importances and model parameters
# Feature importances (Gini importance)
- feature_importances = model.feature_importances_
+ feature_importances = {"gini": model.feature_importances_}
+ return pd.DataFrame(feature_importances, index=training_data.feature_names)
- feature_importance = xr.DataArray(
- feature_importances,
- dims=["feature"],
- coords={"feature": training_data.feature_names},
- name="feature_importance",
- attrs={"description": "Gini importance (impurity-based feature importance)."},
- )
- # cuML RF doesn't expose individual trees, so we store model parameters instead
- n_estimators = model.n_estimators
- max_depth = model.max_depth
-
- # OOB score if available
- oob_score = None
- if hasattr(model, "oob_score_") and model.oob_score:
- oob_score = float(model.oob_score_)
-
- # cuML RandomForest doesn't provide per-tree statistics like sklearn
- # Store what we have: feature importances and model configuration
- attrs = {
- "description": "Inner state of the best RandomForestClassifier from RandomizedSearchCV (cuML).",
- "n_estimators": int(n_estimators),
- "note": "cuML RandomForest does not expose individual tree statistics like sklearn",
- }
-
- # Only add optional attributes if they have values
- if max_depth is not None:
- attrs["max_depth"] = int(max_depth)
- if oob_score is not None:
- attrs["oob_score"] = oob_score
-
- state = xr.Dataset(
- {
- "feature_importance": feature_importance,
- },
- attrs=attrs,
- )
- return state
+def get_splitter(
+ splitter: Splitter, n_splits: int
+) -> (
+ KFold
+ | StratifiedKFold
+ | ShuffleSplit
+ | StratifiedShuffleSplit
+ | GroupKFold
+ | StratifiedGroupKFold
+ | GroupShuffleSplit
+):
+ """Get a scikit-learn splitter object based on the specified splitter type."""
+ match splitter:
+ case "kfold":
+ return KFold(n_splits=n_splits, shuffle=True, random_state=42)
+ case "shuffle":
+ return ShuffleSplit(n_splits=n_splits, test_size=0.2, random_state=42)
+ case "stratified":
+ return StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
+ case "stratified_shuffle":
+ return StratifiedShuffleSplit(n_splits=n_splits, test_size=0.2, random_state=42)
+ case "group":
+ return GroupKFold(n_splits=n_splits, shuffle=True, random_state=42)
+ case "stratified_group":
+ return StratifiedGroupKFold(n_splits=n_splits, shuffle=True, random_state=42)
+ case "shuffle_group":
+ return GroupShuffleSplit(n_splits=n_splits, test_size=0.2, random_state=42)
+ case _:
+ raise ValueError(f"Unsupported splitter type: {splitter}")
diff --git a/src/entropice/ml/training.py b/src/entropice/ml/randomsearch.py
similarity index 68%
rename from src/entropice/ml/training.py
rename to src/entropice/ml/randomsearch.py
index 37c4499..2b1d5ba 100644
--- a/src/entropice/ml/training.py
+++ b/src/entropice/ml/randomsearch.py
@@ -1,8 +1,7 @@
-"""Training of classification models training."""
+"""DEPRECATED!!! Training of classification models training."""
import pickle
from dataclasses import asdict, dataclass
-from functools import partial
from pathlib import Path
import cyclopts
@@ -13,15 +12,7 @@ import xarray as xr
from rich import pretty, traceback
from sklearn import set_config
from sklearn.metrics import (
- accuracy_score,
confusion_matrix,
- f1_score,
- jaccard_score,
- mean_absolute_error,
- mean_squared_error,
- precision_score,
- r2_score,
- recall_score,
)
from sklearn.model_selection import KFold, RandomizedSearchCV
from stopuhr import stopwatch
@@ -30,11 +21,13 @@ from entropice.ml.dataset import DatasetEnsemble, SplittedArrays
from entropice.ml.inference import predict_proba
from entropice.ml.models import (
extract_espa_feature_importance,
+ extract_espa_state,
extract_rf_feature_importance,
extract_xgboost_feature_importance,
get_model_hpo_config,
)
-from entropice.utils.paths import get_cv_results_dir
+from entropice.utils.metrics import get_metrics, metric_functions
+from entropice.utils.paths import get_training_results_dir
from entropice.utils.types import Model, TargetDataset, Task
traceback.install()
@@ -44,56 +37,9 @@ pretty.install()
cli = cyclopts.App("entropice-training", config=cyclopts.config.Toml("training-config.toml")) # ty:ignore[invalid-argument-type]
-def _get_metrics(task: Task) -> tuple[list[str], str]:
- """Get the list of metrics for a given task."""
- if task == "binary":
- return ["accuracy", "recall", "precision", "f1", "jaccard"], "f1"
- elif task in ["count_regimes", "density_regimes"]:
- return [
- "accuracy", # equals "f1_micro", "precision_micro", "recall_micro", "recall_weighted"
- "f1_macro",
- "f1_weighted",
- "precision_macro",
- "precision_weighted",
- "recall_macro",
- "jaccard_micro",
- "jaccard_macro",
- "jaccard_weighted",
- ], "f1_weighted"
- else:
- return [
- "neg_mean_squared_error",
- "neg_mean_absolute_error",
- "r2",
- ], "r2"
-
-
-# Compute other metrics - using predictions directly instead of re-predicting for each metric
-# Use functools.partial for cleaner metric definitions with non-default parameters
-_metric_functions = {
- "accuracy": accuracy_score,
- "recall": recall_score,
- "precision": precision_score,
- "f1": f1_score,
- "jaccard": jaccard_score,
- "recall_macro": partial(recall_score, average="macro"),
- "recall_weighted": partial(recall_score, average="weighted"),
- "precision_macro": partial(precision_score, average="macro"),
- "precision_weighted": partial(precision_score, average="weighted"),
- "f1_macro": partial(f1_score, average="macro"),
- "f1_weighted": partial(f1_score, average="weighted"),
- "jaccard_micro": partial(jaccard_score, average="micro"),
- "jaccard_macro": partial(jaccard_score, average="macro"),
- "jaccard_weighted": partial(jaccard_score, average="weighted"),
- "neg_mean_squared_error": mean_squared_error,
- "neg_mean_absolute_error": mean_absolute_error,
- "r2": r2_score,
-}
-
-
@cyclopts.Parameter("*")
@dataclass(frozen=True, kw_only=True)
-class CVSettings:
+class RunSettings:
"""Cross-validation settings for model training."""
n_iter: int = 2000
@@ -103,7 +49,7 @@ class CVSettings:
@dataclass(frozen=True, kw_only=True)
-class TrainingSettings(DatasetEnsemble, CVSettings):
+class TrainingSettings(DatasetEnsemble, RunSettings):
"""Helper Wrapper to store combined training and dataset ensemble settings."""
param_grid: dict
@@ -115,14 +61,14 @@ class TrainingSettings(DatasetEnsemble, CVSettings):
@cli.default
def random_cv(
dataset_ensemble: DatasetEnsemble,
- settings: CVSettings = CVSettings(),
+ settings: RunSettings = RunSettings(),
experiment: str | None = None,
) -> Path:
"""Perform random cross-validation on the training dataset.
Args:
dataset_ensemble (DatasetEnsemble): The dataset ensemble configuration.
- settings (CVSettings): The cross-validation settings.
+ settings (RunSettings): The cross-validation settings.
experiment (str | None): Optional experiment name for results directory.
"""
@@ -136,7 +82,7 @@ def random_cv(
model_hpo_config = get_model_hpo_config(settings.model, settings.task)
print(f"Using model: {settings.model} with parameters: {model_hpo_config.hp_config}")
cv = KFold(n_splits=5, shuffle=True, random_state=42)
- metrics, refit = _get_metrics(settings.task)
+ metrics, refit = get_metrics(settings.task)
search = RandomizedSearchCV(
model_hpo_config.model,
model_hpo_config.search_space,
@@ -175,12 +121,14 @@ def random_cv(
)
print(f"{refit.replace('_', ' ').capitalize()} on test set: {test_score:.3f}")
- results_dir = get_cv_results_dir(
+ results_dir = get_training_results_dir(
experiment=experiment,
name="random_search",
grid=dataset_ensemble.grid,
level=dataset_ensemble.level,
task=settings.task,
+ target=settings.target,
+ model_type=settings.model,
)
# Store the search settings
@@ -221,9 +169,9 @@ def random_cv(
# Compute and StoreMetrics
y = training_data.y.as_numpy()
- test_metrics = {metric: _metric_functions[metric](y.test, y_pred.test) for metric in metrics}
- train_metrics = {metric: _metric_functions[metric](y.train, y_pred.train) for metric in metrics}
- combined_metrics = {metric: _metric_functions[metric](y.combined, y_pred.combined) for metric in metrics}
+ test_metrics = {metric: metric_functions[metric](y.test, y_pred.test) for metric in metrics}
+ train_metrics = {metric: metric_functions[metric](y.train, y_pred.train) for metric in metrics}
+ combined_metrics = {metric: metric_functions[metric](y.combined, y_pred.combined) for metric in metrics}
all_metrics = {
"test_metrics": test_metrics,
"train_metrics": train_metrics,
@@ -255,31 +203,30 @@ def random_cv(
# Get the inner state of the best estimator
if settings.model == "espa":
- state = extract_espa_feature_importance(best_estimator, training_data)
+ state = extract_espa_state(best_estimator, training_data)
state_file = results_dir / "best_estimator_state.nc"
print(f"Storing best estimator state to {state_file}")
state.to_netcdf(state_file, engine="h5netcdf")
+ fi = extract_espa_feature_importance(best_estimator, training_data)
+ fi_file = results_dir / "best_estimator_feature_importance.parquet"
+ print(f"Storing best estimator feature importance to {fi_file}")
+ fi.to_parquet(fi_file)
elif settings.model == "xgboost":
- state = extract_xgboost_feature_importance(best_estimator, training_data)
- state_file = results_dir / "best_estimator_state.nc"
- print(f"Storing best estimator state to {state_file}")
- state.to_netcdf(state_file, engine="h5netcdf")
+ fi = extract_xgboost_feature_importance(best_estimator, training_data)
+ fi_file = results_dir / "best_estimator_feature_importance.parquet"
+ print(f"Storing best estimator feature importance to {fi_file}")
+ fi.to_parquet(fi_file)
elif settings.model == "rf":
- state = extract_rf_feature_importance(best_estimator, training_data)
- state_file = results_dir / "best_estimator_state.nc"
- print(f"Storing best estimator state to {state_file}")
- state.to_netcdf(state_file, engine="h5netcdf")
+ fi = extract_rf_feature_importance(best_estimator, training_data)
+ fi_file = results_dir / "best_estimator_feature_importance.parquet"
+ print(f"Storing best estimator feature importance to {fi_file}")
+ fi.to_parquet(fi_file)
# Predict probabilities for all cells
print("Predicting probabilities for all cells...")
- preds = predict_proba(dataset_ensemble, model=best_estimator, device=device)
- if training_data.targets["y"].dtype == "category":
- preds["predicted"] = preds["predicted"].astype("category")
- preds["predicted"] = preds["predicted"].cat.set_categories(
- training_data.targets["y"].cat.categories, ordered=True
- )
+ preds = predict_proba(dataset_ensemble, model=best_estimator, task=settings.task, device=device)
print(f"Predicted probabilities DataFrame with {len(preds)} entries.")
preds_file = results_dir / "predicted_probabilities.parquet"
print(f"Storing predicted probabilities to {preds_file}")
diff --git a/src/entropice/utils/metrics.py b/src/entropice/utils/metrics.py
new file mode 100644
index 0000000..671f63d
--- /dev/null
+++ b/src/entropice/utils/metrics.py
@@ -0,0 +1,63 @@
+"""Metrics for model evaluation and hyperparameter optimization."""
+
+from functools import partial
+
+from sklearn.metrics import (
+ accuracy_score,
+ f1_score,
+ jaccard_score,
+ mean_absolute_error,
+ mean_squared_error,
+ precision_score,
+ r2_score,
+ recall_score,
+)
+
+from entropice.utils.types import Task
+
+
+def get_metrics(task: Task) -> tuple[list[str], str]:
+ """Get the list of metrics for a given task."""
+ if task == "binary":
+ return ["accuracy", "recall", "precision", "f1", "jaccard"], "f1"
+ elif task in ["count_regimes", "density_regimes"]:
+ return [
+ "accuracy", # equals "f1_micro", "precision_micro", "recall_micro", "recall_weighted"
+ "f1_macro",
+ "f1_weighted",
+ "precision_macro",
+ "precision_weighted",
+ "recall_macro",
+ "jaccard_micro",
+ "jaccard_macro",
+ "jaccard_weighted",
+ ], "f1_weighted"
+ else:
+ return [
+ "neg_mean_squared_error",
+ "neg_mean_absolute_error",
+ "r2",
+ ], "r2"
+
+
+# Compute other metrics - using predictions directly instead of re-predicting for each metric
+# Use functools.partial for cleaner metric definitions with non-default parameters
+metric_functions = {
+ "accuracy": accuracy_score,
+ "recall": recall_score,
+ "precision": precision_score,
+ "f1": f1_score,
+ "jaccard": jaccard_score,
+ "recall_macro": partial(recall_score, average="macro"),
+ "recall_weighted": partial(recall_score, average="weighted"),
+ "precision_macro": partial(precision_score, average="macro"),
+ "precision_weighted": partial(precision_score, average="weighted"),
+ "f1_macro": partial(f1_score, average="macro"),
+ "f1_weighted": partial(f1_score, average="weighted"),
+ "jaccard_micro": partial(jaccard_score, average="micro"),
+ "jaccard_macro": partial(jaccard_score, average="macro"),
+ "jaccard_weighted": partial(jaccard_score, average="weighted"),
+ "neg_mean_squared_error": mean_squared_error,
+ "neg_mean_absolute_error": mean_absolute_error,
+ "r2": r2_score,
+}
diff --git a/src/entropice/utils/paths.py b/src/entropice/utils/paths.py
index 30da6a1..0257b35 100644
--- a/src/entropice/utils/paths.py
+++ b/src/entropice/utils/paths.py
@@ -6,7 +6,7 @@ import os
from pathlib import Path
from typing import Literal
-from entropice.utils.types import Grid, Task, TemporalMode
+from entropice.utils.types import Grid, Model, TargetDataset, Task, TemporalMode
DATA_DIR = (
Path(os.environ.get("FAST_DATA_DIR", None) or os.environ.get("DATA_DIR", None) or "data").resolve() / "entropice"
@@ -147,12 +147,14 @@ def get_dataset_stats_cache() -> Path:
return cache_dir / "dataset_stats.pckl"
-def get_cv_results_dir(
+def get_training_results_dir(
experiment: str | None,
name: str,
grid: Grid,
level: int,
task: Task,
+ target: TargetDataset,
+ model_type: Model | None = None,
) -> Path:
gridname = _get_gridname(grid, level)
now = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
@@ -161,24 +163,9 @@ def get_cv_results_dir(
experiment_dir.mkdir(parents=True, exist_ok=True)
else:
experiment_dir = RESULTS_DIR
- results_dir = experiment_dir / f"{gridname}_{name}_cv{now}_{task}"
- results_dir.mkdir(parents=True, exist_ok=True)
- return results_dir
-
-
-def get_autogluon_results_dir(
- experiment: str | None,
- grid: Grid,
- level: int,
- task: Task,
-) -> Path:
- gridname = _get_gridname(grid, level)
- now = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
- if experiment is not None:
- experiment_dir = RESULTS_DIR / experiment
- experiment_dir.mkdir(parents=True, exist_ok=True)
- else:
- experiment_dir = RESULTS_DIR
- results_dir = experiment_dir / f"{gridname}_autogluon_{now}_{task}"
+ parts = [gridname, name, now, task, target]
+ if model_type is not None:
+ parts.append(model_type)
+ results_dir = experiment_dir / "_".join(parts)
results_dir.mkdir(parents=True, exist_ok=True)
return results_dir
diff --git a/src/entropice/utils/training.py b/src/entropice/utils/training.py
new file mode 100644
index 0000000..46b33ee
--- /dev/null
+++ b/src/entropice/utils/training.py
@@ -0,0 +1,238 @@
+import pickle
+from dataclasses import asdict, dataclass
+from functools import cached_property
+from pathlib import Path
+from typing import Any, Literal
+
+import cupy as cp
+import geopandas as gpd
+import numpy as np
+import pandas as pd
+import toml
+import torch
+import xarray as xr
+from shap import Explanation
+
+from entropice.ml.dataset import DatasetEnsemble, TrainingSet
+from entropice.ml.models import (
+ HPConfig,
+ extract_espa_state,
+ get_search_space,
+)
+from entropice.utils.types import HPSearch, Model, Scaler, Splitter, TargetDataset, Task
+
+type ndarray = np.ndarray | torch.Tensor | cp.ndarray
+
+
+def move_data_to_device(data: ndarray, device: Literal["torch", "cuda", "cpu"]) -> ndarray:
+ """Move the given data to the specified device (CPU, CUDA, or PyTorch tensor)."""
+ match device:
+ case "torch":
+ torch_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+ return torch.asarray(data, device=torch_device)
+ case "cuda":
+ if isinstance(data, cp.ndarray):
+ return data
+ with cp.cuda.Device(0):
+ return cp.asarray(data)
+ case "cpu":
+ if isinstance(data, np.ndarray):
+ return data
+ elif isinstance(data, torch.Tensor):
+ return data.cpu().numpy()
+ elif isinstance(data, cp.ndarray):
+ return data.get()
+ case _:
+ raise ValueError(f"Unknown device: {device}")
+
+
+@dataclass
+class HPOCV:
+ method: HPSearch
+ splitter: Splitter
+ scaler: Scaler
+ normalize: bool
+ n_iter: int
+ hpconfig: HPConfig
+
+ @property
+ def search_space(self):
+ return get_search_space(self.hpconfig)
+
+
+@dataclass
+class AutoML:
+ time_budget: int
+ preset: str
+ hpo: bool
+
+
+@dataclass
+class Training:
+ """Configuration and results of a training run.
+
+ A training run involves the complete approach of training a machine learning model, currently:
+
+ 1. A hyperparameter optimization using cross-validation
+ 2. Use of AutoML techniques
+
+ Thus, a training run is always defined as:
+
+ f(dataset, method) -> (model, metrics)
+
+ Metrics refer to a simple dataframe in long format, with the columns: "metric", "split", "value".
+ Split is either "train", "test" or "complete".
+ """
+
+ path: Path
+ dataset: DatasetEnsemble
+ method: HPOCV | AutoML
+ task: Task
+ target: TargetDataset
+ training_set: TrainingSet # TODO: Store Training Set to improve loading time (?)
+ model: Any
+ model_type: Model
+ metrics: pd.DataFrame
+ feature_importance: pd.DataFrame
+ shap_explanation: Explanation
+ predictions: gpd.GeoDataFrame
+ confusion_matrix: xr.Dataset | None # only for classification tasks
+ cv_results: pd.DataFrame | None # only for HPOCV
+ leaderboard: pd.DataFrame | None # only for AutoGluon
+
+ def __repr__(self) -> str: # noqa: D105
+ return (
+ f"Training("
+ f"path={self.path}, "
+ f"dataset={self.dataset.grid}-{self.dataset.level}{self.dataset.members}, "
+ f"method={type(self.method).__name__}, "
+ f"task={self.task}, "
+ f"target={self.target})"
+ )
+
+ @cached_property
+ def metric_names(self) -> list[str]:
+ """Get the list of metric names from the metrics DataFrame."""
+ return self.metrics["metric"].unique().tolist()
+
+ @property
+ def get_state(self) -> xr.Dataset | pd.DataFrame | None:
+ """Get the inner state of the trained model, if available."""
+ if self.model_type == "espa":
+ return extract_espa_state(self.model, self.training_set)
+ else:
+ return None
+
+ def save(self):
+ """Save the training results to the specified path."""
+ self.path.mkdir(parents=True, exist_ok=True)
+ config_file = self.path / "training_config.toml"
+ model_file = self.path / "model.pkl"
+ metrics_file = self.path / "metrics.parquet"
+ feature_importance_file = self.path / "feature_importance.parquet"
+ explanations_file = self.path / "shap_explanation.pkl"
+ predictions_file = self.path / "predictions.parquet"
+ # Save config
+ with open(config_file, "w") as f:
+ toml.dump(
+ {
+ "dataset": asdict(self.dataset),
+ "method": asdict(self.method),
+ "method_type": type(self.method).__name__,
+ "task": self.task,
+ "target": self.target,
+ "model_type": self.model_type,
+ },
+ f,
+ )
+
+ # Save model, metrics, explanations and predictions
+ model_file.write_bytes(pickle.dumps(self.model))
+ self.metrics.to_parquet(metrics_file)
+ self.feature_importance.to_parquet(feature_importance_file)
+ explanations_file.write_bytes(pickle.dumps(self.shap_explanation))
+ self.predictions.to_parquet(predictions_file)
+
+ # Save the confusion matrix if it exists
+ if self.confusion_matrix is not None:
+ cm_file = self.path / "confusion_matrix.nc"
+ self.confusion_matrix.to_netcdf(cm_file, engine="h5netcdf")
+
+ if self.cv_results is not None:
+ results_file = self.path / "search_results.parquet"
+ self.cv_results.to_parquet(results_file)
+
+ # Save the leaderboard if it exists
+ if self.leaderboard is not None:
+ leaderboard_file = self.path / "leaderboard.parquet"
+ self.leaderboard.to_parquet(leaderboard_file)
+
+ @classmethod
+ def load(cls, path: Path, device: Literal["cpu", "cuda"] = "cpu") -> "Training":
+ """Load a training run from the specified path."""
+ config_file = path / "training_config.toml"
+ model_file = path / "model.pkl"
+ metrics_file = path / "metrics.parquet"
+ feature_importance_file = path / "feature_importance.parquet"
+ predictions_file = path / "predictions.parquet"
+ cm_file = path / "confusion_matrix.nc"
+ cv_results_file = path / "search_results.parquet"
+ leaderboard_file = path / "leaderboard.parquet"
+
+ # Load config
+ with open(config_file) as f:
+ config = toml.load(f)
+
+ task = config["task"]
+ target = config["target"]
+ model_type = config["model_type"]
+
+ dataset = DatasetEnsemble(**config["dataset"])
+ training_set = dataset.create_training_set(task, target, device)
+
+ method_type = config["method_type"]
+ if method_type == "HPOCV":
+ method = HPOCV(**config["method"])
+ elif method_type == "AutoML":
+ method = AutoML(**config["method"])
+ else:
+ raise ValueError(f"Unknown method type: {method_type}")
+
+ # Load model, metrics, explanations and predictions
+ model = pickle.loads(model_file.read_bytes())
+ metrics = pd.read_parquet(metrics_file)
+ feature_importance = pd.read_parquet(feature_importance_file)
+ shap_explanation = pickle.loads((path / "shap_explanation.pkl").read_bytes())
+ predictions = gpd.read_parquet(predictions_file)
+
+ # Load confusion matrix if it exists
+ confusion_matrix = None
+ if cm_file.exists():
+ confusion_matrix = xr.load_dataset(cm_file, engine="h5netcdf")
+
+ cv_results = None
+ if cv_results_file.exists():
+ cv_results = pd.read_parquet(cv_results_file)
+
+ # Load leaderboard if it exists
+ leaderboard = None
+ if leaderboard_file.exists():
+ leaderboard = pd.read_parquet(leaderboard_file)
+
+ return cls(
+ path=path,
+ dataset=dataset,
+ method=method,
+ task=task,
+ target=target,
+ training_set=training_set,
+ model=model,
+ model_type=model_type,
+ metrics=metrics,
+ feature_importance=feature_importance,
+ shap_explanation=shap_explanation,
+ predictions=predictions,
+ confusion_matrix=confusion_matrix,
+ cv_results=cv_results,
+ leaderboard=leaderboard,
+ )
diff --git a/src/entropice/utils/types.py b/src/entropice/utils/types.py
index 322f2cd..9d5e394 100644
--- a/src/entropice/utils/types.py
+++ b/src/entropice/utils/types.py
@@ -19,10 +19,15 @@ type TargetDataset = Literal["darts_v1", "darts_mllabels"]
type L0SourceDataset = Literal["ArcticDEM", "ERA5", "AlphaEarth"]
type L2SourceDataset = Literal["ArcticDEM", "ERA5-shoulder", "ERA5-seasonal", "ERA5-yearly", "AlphaEarth"]
type Task = Literal["binary", "count_regimes", "density_regimes", "count", "density"]
-# TODO: Consider implementing a "timeseries" temporal mode
+# TODO: Consider implementing a "timeseries" and "event" temporal mode
type TemporalMode = Literal["feature", "synopsis", 2018, 2019, 2020, 2021, 2022, 2023]
-type Model = Literal["espa", "xgboost", "rf", "knn"]
+type Model = Literal["espa", "xgboost", "rf", "knn", "autogluon"]
type Stage = Literal["train", "inference", "visualization"]
+type HPSearch = Literal["random"] # TODO Grid and Bayesian search
+type Splitter = Literal[
+ "kfold", "stratified", "shuffle", "stratified_shuffle", "group", "stratified_group", "shuffle_group"
+]
+type Scaler = Literal["standard", "minmax", "maxabs", "quantile", "none"]
@dataclass(frozen=True)
diff --git a/tests/test_training.py b/tests/test_training.py
index 4fd104a..1d2fc2b 100644
--- a/tests/test_training.py
+++ b/tests/test_training.py
@@ -29,7 +29,7 @@ import shutil
import pytest
from entropice.ml.dataset import DatasetEnsemble
-from entropice.ml.training import CVSettings, random_cv
+from entropice.ml.randomsearch import RunSettings, random_cv
from entropice.utils.types import Model, Task
@@ -90,7 +90,7 @@ class TestRandomCV:
- All output files are created
"""
# Use darts_v1 as the primary target for all tests
- settings = CVSettings(
+ settings = RunSettings(
n_iter=3,
task=task,
target="darts_v1",
@@ -141,7 +141,7 @@ class TestRandomCV:
- xgboost: Uses CUDA without array API dispatch
- rf/knn: GPU-accelerated via cuML
"""
- settings = CVSettings(
+ settings = RunSettings(
n_iter=3,
task="binary", # Simple binary task for device testing
target="darts_v1",
@@ -168,7 +168,7 @@ class TestRandomCV:
def test_random_cv_with_mllabels(self, test_ensemble, cleanup_results):
"""Test random_cv with multi-label target dataset."""
- settings = CVSettings(
+ settings = RunSettings(
n_iter=3,
task="binary",
target="darts_mllabels",
@@ -199,7 +199,7 @@ class TestRandomCV:
add_lonlat=True,
)
- settings = CVSettings(
+ settings = RunSettings(
n_iter=3,
task="binary",
target="darts_v1",