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",