From cf35fdee085bcb21ad101d2a6078c7779694fee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=B4ng-Lan=20Botterman?= Date: Thu, 5 Sep 2024 12:37:28 +0200 Subject: [PATCH] check ruff --- .flake8 | 9 - .github/workflows/publish.yml | 23 +- .github/workflows/test.yml | 34 +- .github/workflows/test_quick.yml | 47 +- .pre-commit-config.yaml | 20 +- Makefile | 33 +- environment.ci.yml | 18 - examples/tutorials/plot_tuto_benchmark_TS.py | 9 +- examples/tutorials/plot_tuto_categorical.py | 12 +- .../tutorials/plot_tuto_diffusion_models.py | 12 +- .../tutorials/plot_tuto_hole_generator.py | 9 +- examples/tutorials/plot_tuto_mcar.py | 6 +- examples/tutorials/plot_tuto_mean_median.py | 23 +- poetry.lock | 1549 +++++++++-------- pyproject.toml | 56 +- qolmat/analysis/holes_characterization.py | 47 +- qolmat/benchmark/comparator.py | 66 +- qolmat/benchmark/hyperparameters.py | 67 +- qolmat/benchmark/metrics.py | 372 ++-- qolmat/benchmark/missing_patterns.py | 324 +++- qolmat/imputations/diffusions/base.py | 95 +- qolmat/imputations/diffusions/ddpms.py | 353 ++-- qolmat/imputations/diffusions/utils.py | 5 +- qolmat/imputations/em_sampler.py | 472 +++-- qolmat/imputations/imputers.py | 776 ++++++--- qolmat/imputations/imputers_pytorch.py | 238 ++- qolmat/imputations/preprocessing.py | 210 ++- qolmat/imputations/rpca/rpca.py | 12 +- qolmat/imputations/rpca/rpca_noisy.py | 132 +- qolmat/imputations/rpca/rpca_pcp.py | 46 +- qolmat/imputations/rpca/rpca_utils.py | 35 +- qolmat/imputations/softimpute.py | 51 +- qolmat/utils/algebra.py | 35 +- qolmat/utils/data.py | 246 ++- qolmat/utils/exceptions.py | 59 +- qolmat/utils/plot.py | 94 +- qolmat/utils/utils.py | 119 +- tests/analysis/test_holes_characterization.py | 15 +- tests/benchmark/test_comparator.py | 23 +- tests/benchmark/test_hyperparameters.py | 55 +- tests/benchmark/test_metrics.py | 122 +- tests/benchmark/test_missing_patterns.py | 36 +- tests/imputations/rpca/test_rpca.py | 23 +- tests/imputations/rpca/test_rpca_noisy.py | 48 +- tests/imputations/rpca/test_rpca_pcp.py | 12 +- tests/imputations/rpca/test_rpca_utils.py | 10 +- tests/imputations/test_em_sampler.py | 89 +- tests/imputations/test_imputers.py | 79 +- tests/imputations/test_imputers_diffusions.py | 133 +- tests/imputations/test_imputers_pytorch.py | 32 +- tests/imputations/test_preprocessing.py | 27 +- tests/imputations/test_softimpute.py | 49 +- tests/utils/test_algebra.py | 9 +- tests/utils/test_data.py | 80 +- tests/utils/test_exceptions.py | 1 - tests/utils/test_plot.py | 29 +- tests/utils/test_utils.py | 19 +- 57 files changed, 4169 insertions(+), 2436 deletions(-) delete mode 100644 .flake8 delete mode 100644 environment.ci.yml diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 33a40614..00000000 --- a/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -exclude = .git,__pycache__,.vscode -max-line-length=99 -ignore=E302,E305,W503,E203,E731,E402,E266,E712,F401,F821 -indent-size = 4 -per-file-ignores= - qolmat/imputations/imputers.py:F401 - */__init__.py:F401 - examples/test.py:F401 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b3177c72..39ba5324 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,13 +1,11 @@ -name: Publish Package on PYPI +name: Publish Package on PyPI on: release: types: [published] - jobs: deploy: - runs-on: ubuntu-latest steps: @@ -16,14 +14,19 @@ jobs: uses: actions/setup-python@v4 with: python-version: '3.10' + - name: Install Poetry + run: | + curl -sSL https://install.python-poetry.org | python3 - + echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine + poetry install - name: Build package - run: python setup.py sdist bdist_wheel + run: | + poetry build - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + env: + PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + run: | + poetry config pypi-token.pypi $PYPI_TOKEN + poetry publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9081326e..36b15540 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,8 +3,7 @@ name: Unit tests on: push: branches: - -dev - -main + - "**" pull_request: types: [opened, synchronize, reopened, ready_for_review] workflow_dispatch: @@ -22,24 +21,23 @@ jobs: shell: bash -l {0} steps: - - name: Git clone + - name: Checkout uses: actions/checkout@v3 - - name: Set up venv for ci - uses: conda-incubator/setup-miniconda@v2 + - name: Python + uses: actions/setup-python@v4 with: - python-version: ${{matrix.python-version}} - environment-file: environment.ci.yml - - name: Lint with flake8 - run: | - flake8 - - name: Test with pytest - run: | - make coverage - - name: typing with mypy - run: | - mypy qolmat - echo you should uncomment mypy qolmat and delete this line - - name: Upload coverage reports to Codecov + python-version: ${{ matrix.python-version }} + - name: Poetry + uses: snok/install-poetry@v1 + with: + version: 1.8.3 + - name: Lock + run: poetry lock --no-update + - name: Install + run: poetry install + - name: Checkers + run: make checkers + - name: Codecov uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/test_quick.yml b/.github/workflows/test_quick.yml index 40f58f5a..205977a4 100644 --- a/.github/workflows/test_quick.yml +++ b/.github/workflows/test_quick.yml @@ -19,7 +19,7 @@ jobs: shell: bash -l {0} steps: - - name: Git clone + - name: Checkout uses: actions/checkout@v3 # See caching environments @@ -29,40 +29,27 @@ jobs: with: miniforge-variant: Mambaforge miniforge-version: latest - activate-environment: env_qolmat_ci use-mamba: true + python-version: ${{ matrix.python-version }} - name: Get Date id: get-date run: echo "today=$(/bin/date -u '+%Y%m%d')" >> $GITHUB_OUTPUT - - name: Cache Conda env + - name: Cache Poetry dependencies uses: actions/cache@v2 with: - path: ${{ env.CONDA }}/envs - key: - conda-${{ runner.os }}--${{ runner.arch }}--${{ - steps.get-date.outputs.today }}-${{ - hashFiles('environment.ci.yml') }}-${{ env.CACHE_NUMBER - }} - env: - # Increase this value to reset cache if environment.ci.yml has not changed - CACHE_NUMBER: 0 - id: cache - - - name: Update environment - run: mamba env update -n env_qolmat_ci -f environment.ci.yml - if: steps.cache.outputs.cache-hit != 'true' - - - name: Lint with flake8 - run: | - flake8 - - name: Test with pytest - run: | - make coverage - - name: Test docstrings - run: make doctest - - name: typing with mypy - run: | - mypy qolmat - echo you should uncomment mypy qolmat and delete this line + path: | + ~/.cache/pypoetry/cache + ~/.cache/pypoetry/artifacts + key: poetry-${{ runner.os }}-cache-${{ steps.get-date.outputs.today }}-${{ hashFiles('poetry.lock') }} + restore-keys: | + poetry-${{ runner.os }}-cache- + + - name: Install Poetry + run : + mamba install -c conda-forge poetry -y + poetry install + + - name: Checkers + run: make check-coverage check-types diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 62948350..68c1acf3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,20 +8,8 @@ repos: exclude: (docs/) - id: trailing-whitespace exclude: (docs/) - - repo: https://github.com/psf/black - rev: 22.8.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.3 hooks: - - id: black - args: - - "-l 99" - # Flake8 - - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.1.1 - hooks: - - id: mypy - args: [--ignore-missing-imports] - additional_dependencies: [types-requests] + - id: ruff + - id: ruff-format diff --git a/Makefile b/Makefile index c08e0d40..e0ca5828 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,34 @@ -coverage: - pytest --cov-branch --cov=qolmat --cov-report=xml tests -doctest: - pytest --doctest-modules --pyargs qolmat +check-coverage: + poetry run pytest --cov-branch --cov=qolmat/ --cov-report=xml tests/ -doc: - make html -C docs +check-poetry: + poetry check --lock + +check-quality: + poetry run ruff check qolmat/ tests/ + +check-security: + poetry run bandit --recursive --configfile=pyproject.toml qolmat/ + +check-tests: + poetry run pytest tests/ + +check-types: + poetry run mypy qolmat/ tests/ + +checkers: check-coverage check-types clean: rm -rf .mypy_cache .pytest_cache .coverage* rm -rf **__pycache__ make clean -C docs + +coverage: + poetry run pytest --cov-branch --cov=qolmat --cov-report=xml tests + +doc: + make html -C docs + +doctest: + poetry run pytest --doctest-modules --pyargs qolmat diff --git a/environment.ci.yml b/environment.ci.yml deleted file mode 100644 index 86949837..00000000 --- a/environment.ci.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: env_qolmat_ci -channels: - - defaults - - conda-forge -dependencies: - - codecov - - flake8 - - matplotlib - - mypy - - numpy - - numpydoc - - pytest - - pytest-cov - - pytest-mock - - pip - - pip: - - torch - - -e . diff --git a/examples/tutorials/plot_tuto_benchmark_TS.py b/examples/tutorials/plot_tuto_benchmark_TS.py index f205d08a..1fdbbddb 100644 --- a/examples/tutorials/plot_tuto_benchmark_TS.py +++ b/examples/tutorials/plot_tuto_benchmark_TS.py @@ -1,5 +1,4 @@ -""" -========================= +"""========================= Benchmark for time series ========================= @@ -14,18 +13,18 @@ # First import some libraries import numpy as np -import pandas as pd np.random.seed(1234) -from matplotlib import pyplot as plt import matplotlib.ticker as plticker +from matplotlib import pyplot as plt tab10 = plt.get_cmap("tab10") +from sklearn.linear_model import LinearRegression + from qolmat.benchmark import comparator, missing_patterns from qolmat.imputations import imputers from qolmat.utils import data, plot -from sklearn.linear_model import LinearRegression # %% # 1. Data diff --git a/examples/tutorials/plot_tuto_categorical.py b/examples/tutorials/plot_tuto_categorical.py index b6e993fb..f0491ac8 100644 --- a/examples/tutorials/plot_tuto_categorical.py +++ b/examples/tutorials/plot_tuto_categorical.py @@ -1,5 +1,4 @@ -""" -============================== +"""============================== Benchmark for categorical data ============================== @@ -8,14 +7,13 @@ It comprehends passengers features as well as if they survived the accident. """ -from qolmat.imputations import preprocessing, imputers +from sklearn.pipeline import Pipeline + +from qolmat.benchmark import comparator, missing_patterns +from qolmat.imputations import imputers, preprocessing from qolmat.imputations.imputers import ImputerRegressor -from qolmat.benchmark import missing_patterns -from qolmat.benchmark import comparator from qolmat.utils import data -from sklearn.pipeline import Pipeline - # %% # 1. Titanic dataset # --------------------------------------------------------------- diff --git a/examples/tutorials/plot_tuto_diffusion_models.py b/examples/tutorials/plot_tuto_diffusion_models.py index 317128db..0ff0d80a 100644 --- a/examples/tutorials/plot_tuto_diffusion_models.py +++ b/examples/tutorials/plot_tuto_diffusion_models.py @@ -1,5 +1,4 @@ -""" -=============================================== +"""=============================================== Tutorial for imputers based on diffusion models =============================================== @@ -7,15 +6,14 @@ and :class:`~qolmat.imputations.diffusions.ddpms.TsDDPM` classes. """ -import pandas as pd -import numpy as np import matplotlib.pyplot as plt +import numpy as np +import pandas as pd -from qolmat.utils import data from qolmat.benchmark import comparator, missing_patterns - -from qolmat.imputations.imputers_pytorch import ImputerDiffusion from qolmat.imputations.diffusions.ddpms import TabDDPM, TsDDPM +from qolmat.imputations.imputers_pytorch import ImputerDiffusion +from qolmat.utils import data # %% # 1. Time-series data diff --git a/examples/tutorials/plot_tuto_hole_generator.py b/examples/tutorials/plot_tuto_hole_generator.py index 07594591..07ea6348 100644 --- a/examples/tutorials/plot_tuto_hole_generator.py +++ b/examples/tutorials/plot_tuto_hole_generator.py @@ -1,5 +1,4 @@ -""" -============================================ +"""============================================ Tutorial for hole generation in tabular data ============================================ @@ -17,13 +16,10 @@ """ from typing import List -from io import BytesIO import matplotlib import matplotlib.pyplot as plt import numpy as np import pandas as pd -import requests -import zipfile from qolmat.benchmark import missing_patterns from qolmat.utils import data @@ -90,6 +86,7 @@ def visualise_missing_values(df_init: pd.DataFrame, df_mask: pd.DataFrame): initial dataframe df_mask : pd.DataFrame masked dataframe + """ df_tot = df_init.copy() df_tot[df_init.notna()] = 0 @@ -117,6 +114,7 @@ def get_holes_sizes_column_wise(data: np.ndarray) -> List[List[int]]: ------- List[List[int]] List of hole size for each column. + """ hole_sizes = [] for col in range(data.shape[1]): @@ -153,6 +151,7 @@ def plot_cdf( list of labels colors : List[str] list of colors + """ _, axs = plt.subplots(1, df.shape[1], sharey=True, figsize=(15, 3)) diff --git a/examples/tutorials/plot_tuto_mcar.py b/examples/tutorials/plot_tuto_mcar.py index c43d1217..a9bddb7f 100644 --- a/examples/tutorials/plot_tuto_mcar.py +++ b/examples/tutorials/plot_tuto_mcar.py @@ -1,5 +1,4 @@ -""" -============================================ +"""============================================ Tutorial for Testing the MCAR Case ============================================ @@ -8,10 +7,9 @@ # %% # First import some libraries -from matplotlib import pyplot as plt - import numpy as np import pandas as pd +from matplotlib import pyplot as plt from scipy.stats import norm from qolmat.analysis.holes_characterization import LittleTest diff --git a/examples/tutorials/plot_tuto_mean_median.py b/examples/tutorials/plot_tuto_mean_median.py index 403b4407..33c36db2 100644 --- a/examples/tutorials/plot_tuto_mean_median.py +++ b/examples/tutorials/plot_tuto_mean_median.py @@ -1,5 +1,4 @@ -""" -======================================================================================== +"""======================================================================================== Comparison of basic imputers ======================================================================================== @@ -21,7 +20,6 @@ from qolmat.imputations import imputers from qolmat.utils import data, plot - # %% # 1. Data # --------------------------------------------------------------- @@ -29,11 +27,14 @@ # Originally, the first 81 columns contain extracted features and # the 82nd column contains the critical temperature which is used as the # target variable. -# The data does not contain missing values; so for the purpose of this notebook, +# The data does not contain missing values; +# so for the purpose of this notebook, # we corrupt the data, with the :func:`qolmat.utils.data.add_holes` function. # In this way, each column has missing values. -df = data.add_holes(data.get_data("Superconductor"), ratio_masked=0.2, mean_size=120) +df = data.add_holes( + data.get_data("Superconductor"), ratio_masked=0.2, mean_size=120 +) # %% # The dataset contains 82 columns. For simplicity, @@ -55,7 +56,9 @@ # a missing (resp. observed) value. plt.figure(figsize=(15, 4)) -plt.imshow(df.notna().values.T, aspect="auto", cmap="binary", interpolation="none") +plt.imshow( + df.notna().values.T, aspect="auto", cmap="binary", interpolation="none" +) plt.yticks(range(len(df.columns)), df.columns) plt.xlabel("Samples", fontsize=12) plt.grid(False) @@ -102,7 +105,9 @@ custom_cmap = matplotlib.colors.ListedColormap(colorsList) plt.figure(figsize=(15, 4)) -plt.imshow(df_tot.values.T, aspect="auto", cmap=custom_cmap, interpolation="none") +plt.imshow( + df_tot.values.T, aspect="auto", cmap=custom_cmap, interpolation="none" +) plt.yticks(range(len(df_tot.columns)), df_tot.columns) plt.xlabel("Samples", fontsize=12) plt.grid(False) @@ -147,7 +152,9 @@ # are relatively poor. Other imputation methods are therefore # necessary (see folder `imputations`). -dfs_imputed = {name: imp.fit_transform(df) for name, imp in dict_imputers.items()} +dfs_imputed = { + name: imp.fit_transform(df) for name, imp in dict_imputers.items() +} for col in cols_to_impute: fig, ax = plt.subplots(figsize=(10, 3)) diff --git a/poetry.lock b/poetry.lock index 9f161fd9..d6c13616 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,19 +13,20 @@ files = [ [[package]] name = "anyio" -version = "4.1.0" +version = "4.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.1.0-py3-none-any.whl", hash = "sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f"}, - {file = "anyio-4.1.0.tar.gz", hash = "sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"}, + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] @@ -139,32 +140,32 @@ test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] [[package]] name = "attrs" -version = "23.2.0" +version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "babel" -version = "2.15.0" +version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" files = [ - {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, - {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] [package.dependencies] @@ -199,6 +200,30 @@ files = [ docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] +[[package]] +name = "bandit" +version = "1.7.9" +description = "Security oriented static analyser for python code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, + {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +PyYAML = ">=5.3.1" +rich = "*" +stevedore = ">=1.20.0" + +[package.extras] +baseline = ["GitPython (>=3.1.30)"] +sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] +toml = ["tomli (>=1.1.0)"] +yaml = ["PyYAML"] + [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -249,76 +274,111 @@ files = [ {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, ] +[[package]] +name = "category-encoders" +version = "2.6.3" +description = "A collection of sklearn transformers to encode categorical variables as numeric" +optional = false +python-versions = "*" +files = [ + {file = "category_encoders-2.6.3-py2.py3-none-any.whl", hash = "sha256:117775f1775e53a67c9e91842ac9100bc364cddc9f4058188796532bc5b11f1c"}, + {file = "category_encoders-2.6.3.tar.gz", hash = "sha256:d9f14705ed4b536eaf9cfc81b76d67a50b2f16f8f3eda498c57d7da19655530c"}, +] + +[package.dependencies] +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} +numpy = ">=1.14.0" +pandas = ">=1.0.5" +patsy = ">=0.5.1" +scikit-learn = ">=0.20.0" +scipy = ">=1.0.0" +statsmodels = ">=0.9.0" + [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "cffi" -version = "1.16.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -445,6 +505,21 @@ files = [ {file = "cloudpickle-3.0.0.tar.gz", hash = "sha256:996d9a482c6fb4f33c1a35335cf8afd065d2a56e973270364840712d9131a882"}, ] +[[package]] +name = "codecov" +version = "2.1.13" +description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"}, + {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"}, +] + +[package.dependencies] +coverage = "*" +requests = ">=2.7.9" + [[package]] name = "colorama" version = "0.4.6" @@ -546,63 +621,83 @@ test-no-images = ["pytest", "pytest-cov", "wurlitzer"] [[package]] name = "coverage" -version = "7.6.0" +version = "7.6.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, - {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, - {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, - {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, - {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, - {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, - {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, - {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, - {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, - {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, - {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, - {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, - {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, - {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, - {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, - {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, - {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, - {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, - {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, - {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, ] [package.dependencies] @@ -613,38 +708,38 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.0" +version = "43.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, - {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, - {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, - {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, - {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, - {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, - {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, - {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, - {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, - {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, - {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, + {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, + {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, + {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, + {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, + {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, + {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, + {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, ] [package.dependencies] @@ -657,7 +752,7 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] @@ -697,33 +792,33 @@ test = ["numba (>=0.56)", "numpy (>=1.22)", "pytest", "pytest-cov", "pytest-subt [[package]] name = "debugpy" -version = "1.8.2" +version = "1.8.5" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7ee2e1afbf44b138c005e4380097d92532e1001580853a7cb40ed84e0ef1c3d2"}, - {file = "debugpy-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f8c3f7c53130a070f0fc845a0f2cee8ed88d220d6b04595897b66605df1edd6"}, - {file = "debugpy-1.8.2-cp310-cp310-win32.whl", hash = "sha256:f179af1e1bd4c88b0b9f0fa153569b24f6b6f3de33f94703336363ae62f4bf47"}, - {file = "debugpy-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:0600faef1d0b8d0e85c816b8bb0cb90ed94fc611f308d5fde28cb8b3d2ff0fe3"}, - {file = "debugpy-1.8.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8a13417ccd5978a642e91fb79b871baded925d4fadd4dfafec1928196292aa0a"}, - {file = "debugpy-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acdf39855f65c48ac9667b2801234fc64d46778021efac2de7e50907ab90c634"}, - {file = "debugpy-1.8.2-cp311-cp311-win32.whl", hash = "sha256:2cbd4d9a2fc5e7f583ff9bf11f3b7d78dfda8401e8bb6856ad1ed190be4281ad"}, - {file = "debugpy-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:d3408fddd76414034c02880e891ea434e9a9cf3a69842098ef92f6e809d09afa"}, - {file = "debugpy-1.8.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:5d3ccd39e4021f2eb86b8d748a96c766058b39443c1f18b2dc52c10ac2757835"}, - {file = "debugpy-1.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62658aefe289598680193ff655ff3940e2a601765259b123dc7f89c0239b8cd3"}, - {file = "debugpy-1.8.2-cp312-cp312-win32.whl", hash = "sha256:bd11fe35d6fd3431f1546d94121322c0ac572e1bfb1f6be0e9b8655fb4ea941e"}, - {file = "debugpy-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:15bc2f4b0f5e99bf86c162c91a74c0631dbd9cef3c6a1d1329c946586255e859"}, - {file = "debugpy-1.8.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:5a019d4574afedc6ead1daa22736c530712465c0c4cd44f820d803d937531b2d"}, - {file = "debugpy-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40f062d6877d2e45b112c0bbade9a17aac507445fd638922b1a5434df34aed02"}, - {file = "debugpy-1.8.2-cp38-cp38-win32.whl", hash = "sha256:c78ba1680f1015c0ca7115671fe347b28b446081dada3fedf54138f44e4ba031"}, - {file = "debugpy-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cf327316ae0c0e7dd81eb92d24ba8b5e88bb4d1b585b5c0d32929274a66a5210"}, - {file = "debugpy-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1523bc551e28e15147815d1397afc150ac99dbd3a8e64641d53425dba57b0ff9"}, - {file = "debugpy-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e24ccb0cd6f8bfaec68d577cb49e9c680621c336f347479b3fce060ba7c09ec1"}, - {file = "debugpy-1.8.2-cp39-cp39-win32.whl", hash = "sha256:7f8d57a98c5a486c5c7824bc0b9f2f11189d08d73635c326abef268f83950326"}, - {file = "debugpy-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:16c8dcab02617b75697a0a925a62943e26a0330da076e2a10437edd9f0bf3755"}, - {file = "debugpy-1.8.2-py2.py3-none-any.whl", hash = "sha256:16e16df3a98a35c63c3ab1e4d19be4cbc7fdda92d9ddc059294f18910928e0ca"}, - {file = "debugpy-1.8.2.zip", hash = "sha256:95378ed08ed2089221896b9b3a8d021e642c24edc8fef20e5d4342ca8be65c00"}, + {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, + {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, + {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, + {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, + {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, + {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, + {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, + {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, + {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, + {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, + {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, + {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, + {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, + {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, + {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, + {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, + {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, + {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, + {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, + {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, + {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, + {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, ] [[package]] @@ -797,13 +892,13 @@ test = ["pytest (>=6)"] [[package]] name = "executing" -version = "2.0.1" +version = "2.1.0" description = "Get the currently executing AST node of a frame, and other information" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, + {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, + {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, ] [package.extras] @@ -839,22 +934,6 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] -[[package]] -name = "flake8" -version = "6.0.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, - {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.10.0,<2.11.0" -pyflakes = ">=3.0.0,<3.1.0" - [[package]] name = "fonttools" version = "4.53.1" @@ -985,13 +1064,13 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.7" +version = "3.8" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, ] [[package]] @@ -1007,13 +1086,13 @@ files = [ [[package]] name = "importlib-metadata" -version = "8.2.0" +version = "8.4.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"}, - {file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"}, + {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, + {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, ] [package.dependencies] @@ -1026,21 +1105,25 @@ test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "p [[package]] name = "importlib-resources" -version = "6.4.0" +version = "6.4.4" description = "Read resources from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, - {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, + {file = "importlib_resources-6.4.4-py3-none-any.whl", hash = "sha256:dda242603d1c9cd836c3368b1174ed74cb4049ecd209e7a1a0104620c18c5c11"}, + {file = "importlib_resources-6.4.4.tar.gz", hash = "sha256:20600c8b7361938dc0bb2d5ec0297802e575df486f5a544fa414da65e13721f7"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +type = ["pytest-mypy"] [[package]] name = "iniconfig" @@ -1055,13 +1138,13 @@ files = [ [[package]] name = "ipykernel" -version = "6.21.0" +version = "6.29.5" description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.8" files = [ - {file = "ipykernel-6.21.0-py3-none-any.whl", hash = "sha256:a9aaefb0cd6f44e3dcaeaafb9222862ae73831f71afd77f465fc83d6199d1888"}, - {file = "ipykernel-6.21.0.tar.gz", hash = "sha256:719eac0f1d86706589ee0910d6f785fd4c1ef123ab8e3466b8961c37991d0ef2"}, + {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, + {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, ] [package.dependencies] @@ -1072,9 +1155,10 @@ ipython = ">=7.23.1" jupyter-client = ">=6.1.12" jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" matplotlib-inline = ">=0.1" +nest-asyncio = "*" packaging = "*" psutil = "*" -pyzmq = ">=17" +pyzmq = ">=24" tornado = ">=6.1" traitlets = ">=5.4.0" @@ -1083,7 +1167,7 @@ cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] pyqt5 = ["pyqt5"] pyside6 = ["pyside6"] -test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov", "pytest-timeout"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] [[package]] name = "ipython" @@ -1137,21 +1221,21 @@ files = [ [[package]] name = "ipywidgets" -version = "8.1.3" +version = "8.1.5" description = "Jupyter interactive widgets" optional = false python-versions = ">=3.7" files = [ - {file = "ipywidgets-8.1.3-py3-none-any.whl", hash = "sha256:efafd18f7a142248f7cb0ba890a68b96abd4d6e88ddbda483c9130d12667eaf2"}, - {file = "ipywidgets-8.1.3.tar.gz", hash = "sha256:f5f9eeaae082b1823ce9eac2575272952f40d748893972956dc09700a6392d9c"}, + {file = "ipywidgets-8.1.5-py3-none-any.whl", hash = "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245"}, + {file = "ipywidgets-8.1.5.tar.gz", hash = "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17"}, ] [package.dependencies] comm = ">=0.1.3" ipython = ">=6.1.0" -jupyterlab-widgets = ">=3.0.11,<3.1.0" +jupyterlab-widgets = ">=3.0.12,<3.1.0" traitlets = ">=4.3.1" -widgetsnbextension = ">=4.0.11,<4.1.0" +widgetsnbextension = ">=4.0.12,<4.1.0" [package.extras] test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] @@ -1190,21 +1274,21 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-ena [[package]] name = "jaraco-context" -version = "5.3.0" +version = "6.0.1" description = "Useful decorators and context managers" optional = false python-versions = ">=3.8" files = [ - {file = "jaraco.context-5.3.0-py3-none-any.whl", hash = "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266"}, - {file = "jaraco.context-5.3.0.tar.gz", hash = "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2"}, + {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, + {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, ] [package.dependencies] "backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["portend", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "jaraco-functools" @@ -1576,13 +1660,13 @@ test = ["pytest", "requests"] [[package]] name = "jupyterlab-widgets" -version = "3.0.11" +version = "3.0.13" description = "Jupyter interactive widgets for JupyterLab" optional = false python-versions = ">=3.7" files = [ - {file = "jupyterlab_widgets-3.0.11-py3-none-any.whl", hash = "sha256:78287fd86d20744ace330a61625024cf5521e1c012a352ddc0a3cdc2348becd0"}, - {file = "jupyterlab_widgets-3.0.11.tar.gz", hash = "sha256:dd5ac679593c969af29c9bed054c24f26842baa51352114736756bc035deee27"}, + {file = "jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54"}, + {file = "jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed"}, ] [[package]] @@ -1609,13 +1693,13 @@ toml = ["toml"] [[package]] name = "keyring" -version = "25.2.1" +version = "25.3.0" description = "Store and access your passwords safely." optional = false python-versions = ">=3.8" files = [ - {file = "keyring-25.2.1-py3-none-any.whl", hash = "sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50"}, - {file = "keyring-25.2.1.tar.gz", hash = "sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b"}, + {file = "keyring-25.3.0-py3-none-any.whl", hash = "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae"}, + {file = "keyring-25.3.0.tar.gz", hash = "sha256:8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef"}, ] [package.dependencies] @@ -1630,120 +1714,130 @@ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] completion = ["shtab (>=1.1.0)"] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "kiwisolver" -version = "1.4.5" +version = "1.4.7" description = "A fast implementation of the Cassowary constraint solver" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, - {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, - {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, - {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, - {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, - {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, - {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, - {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, - {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, - {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, - {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, - {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"}, + {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"}, + {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"}, + {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, ] [[package]] @@ -1781,13 +1875,13 @@ files = [ [[package]] name = "markdown" -version = "3.6" +version = "3.7" description = "Python implementation of John Gruber's Markdown." optional = true python-versions = ">=3.8" files = [ - {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, - {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, ] [package.dependencies] @@ -1965,17 +2059,6 @@ files = [ [package.dependencies] traitlets = "*" -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "mdit-py-plugins" version = "0.4.1" @@ -2019,32 +2102,15 @@ files = [ [[package]] name = "more-itertools" -version = "10.3.0" +version = "10.4.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.8" files = [ - {file = "more-itertools-10.3.0.tar.gz", hash = "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463"}, - {file = "more_itertools-10.3.0-py3-none-any.whl", hash = "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320"}, -] - -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -optional = true -python-versions = "*" -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, + {file = "more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923"}, + {file = "more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27"}, ] -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4)"] -tests = ["pytest (>=4.6)"] - [[package]] name = "mypy" version = "1.1.1" @@ -2544,6 +2610,17 @@ six = "*" [package.extras] test = ["pytest", "pytest-cov", "scipy"] +[[package]] +name = "pbr" +version = "6.1.0" +description = "Python Build Reasonableness" +optional = false +python-versions = ">=2.6" +files = [ + {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, + {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -2833,17 +2910,6 @@ files = [ {file = "py4j-0.10.9.7.tar.gz", hash = "sha256:0b6e5315bb3ada5cf62ac651d107bb2ebc02def3dee9d9548e3baac644ea8dbb"}, ] -[[package]] -name = "pycodestyle" -version = "2.10.0" -description = "Python style guide checker" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, - {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, -] - [[package]] name = "pycparser" version = "2.22" @@ -2855,17 +2921,6 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -[[package]] -name = "pyflakes" -version = "3.0.1" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, - {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, -] - [[package]] name = "pygments" version = "2.18.0" @@ -2882,13 +2937,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyparsing" -version = "3.1.2" +version = "3.1.4" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, - {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, + {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, + {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, ] [package.extras] @@ -3013,13 +3068,13 @@ files = [ [[package]] name = "pywin32-ctypes" -version = "0.2.2" +version = "0.2.3" description = "A (partial) reimplementation of pywin32 using ctypes/cffi" optional = false python-versions = ">=3.6" files = [ - {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, - {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, ] [[package]] @@ -3039,159 +3094,182 @@ files = [ [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] name = "pyzmq" -version = "26.0.3" +version = "26.2.0" description = "Python bindings for 0MQ" optional = false python-versions = ">=3.7" files = [ - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"}, - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"}, - {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"}, - {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"}, - {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"}, - {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"}, - {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"}, - {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"}, - {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"}, + {file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"}, + {file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"}, + {file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"}, + {file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"}, + {file = "pyzmq-26.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win32.whl", hash = "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd"}, + {file = "pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988"}, + {file = "pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"}, + {file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"}, + {file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"}, ] [package.dependencies] @@ -3199,13 +3277,13 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "qtconsole" -version = "5.5.2" +version = "5.6.0" description = "Jupyter Qt console" optional = false python-versions = ">=3.8" files = [ - {file = "qtconsole-5.5.2-py3-none-any.whl", hash = "sha256:42d745f3d05d36240244a04e1e1ec2a86d5d9b6edb16dbdef582ccb629e87e0b"}, - {file = "qtconsole-5.5.2.tar.gz", hash = "sha256:6b5fb11274b297463706af84dcbbd5c92273b1f619e6d25d08874b0a88516989"}, + {file = "qtconsole-5.6.0-py3-none-any.whl", hash = "sha256:c36e0d497a473b67898b96dd38666e645e4594019244263da7b409b84fa2ebb5"}, + {file = "qtconsole-5.6.0.tar.gz", hash = "sha256:4c82120a3b53a3d36e3f76e6a1a26ffddf4e1ce2359d56a19889c55e1d73a436"}, ] [package.dependencies] @@ -3214,7 +3292,6 @@ jupyter-client = ">=4.1" jupyter-core = "*" packaging = "*" pygments = "*" -pyzmq = ">=17.1" qtpy = ">=2.4.0" traitlets = "<5.2.1 || >5.2.1,<5.2.2 || >5.2.2" @@ -3347,116 +3424,162 @@ files = [ {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, ] +[[package]] +name = "rich" +version = "13.8.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, + {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "rpds-py" -version = "0.19.1" +version = "0.20.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:aaf71f95b21f9dc708123335df22e5a2fef6307e3e6f9ed773b2e0938cc4d491"}, - {file = "rpds_py-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca0dda0c5715efe2ab35bb83f813f681ebcd2840d8b1b92bfc6fe3ab382fae4a"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81db2e7282cc0487f500d4db203edc57da81acde9e35f061d69ed983228ffe3b"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8dfa125b60ec00c7c9baef945bb04abf8ac772d8ebefd79dae2a5f316d7850"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271accf41b02687cef26367c775ab220372ee0f4925591c6796e7c148c50cab5"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9bc4161bd3b970cd6a6fcda70583ad4afd10f2750609fb1f3ca9505050d4ef3"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0cf2a0dbb5987da4bd92a7ca727eadb225581dd9681365beba9accbe5308f7d"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b5e28e56143750808c1c79c70a16519e9bc0a68b623197b96292b21b62d6055c"}, - {file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c7af6f7b80f687b33a4cdb0a785a5d4de1fb027a44c9a049d8eb67d5bfe8a687"}, - {file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e429fc517a1c5e2a70d576077231538a98d59a45dfc552d1ac45a132844e6dfb"}, - {file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d2dbd8f4990d4788cb122f63bf000357533f34860d269c1a8e90ae362090ff3a"}, - {file = "rpds_py-0.19.1-cp310-none-win32.whl", hash = "sha256:e0f9d268b19e8f61bf42a1da48276bcd05f7ab5560311f541d22557f8227b866"}, - {file = "rpds_py-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:df7c841813f6265e636fe548a49664c77af31ddfa0085515326342a751a6ba51"}, - {file = "rpds_py-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:902cf4739458852fe917104365ec0efbea7d29a15e4276c96a8d33e6ed8ec137"}, - {file = "rpds_py-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3d73022990ab0c8b172cce57c69fd9a89c24fd473a5e79cbce92df87e3d9c48"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3837c63dd6918a24de6c526277910e3766d8c2b1627c500b155f3eecad8fad65"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cdb7eb3cf3deb3dd9e7b8749323b5d970052711f9e1e9f36364163627f96da58"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26ab43b6d65d25b1a333c8d1b1c2f8399385ff683a35ab5e274ba7b8bb7dc61c"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75130df05aae7a7ac171b3b5b24714cffeabd054ad2ebc18870b3aa4526eba23"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c34f751bf67cab69638564eee34023909380ba3e0d8ee7f6fe473079bf93f09b"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2671cb47e50a97f419a02cd1e0c339b31de017b033186358db92f4d8e2e17d8"}, - {file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c73254c256081704dba0a333457e2fb815364018788f9b501efe7c5e0ada401"}, - {file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4383beb4a29935b8fa28aca8fa84c956bf545cb0c46307b091b8d312a9150e6a"}, - {file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dbceedcf4a9329cc665452db1aaf0845b85c666e4885b92ee0cddb1dbf7e052a"}, - {file = "rpds_py-0.19.1-cp311-none-win32.whl", hash = "sha256:f0a6d4a93d2a05daec7cb885157c97bbb0be4da739d6f9dfb02e101eb40921cd"}, - {file = "rpds_py-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:c149a652aeac4902ecff2dd93c3b2681c608bd5208c793c4a99404b3e1afc87c"}, - {file = "rpds_py-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:56313be667a837ff1ea3508cebb1ef6681d418fa2913a0635386cf29cff35165"}, - {file = "rpds_py-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d1d7539043b2b31307f2c6c72957a97c839a88b2629a348ebabe5aa8b626d6b"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1dc59a5e7bc7f44bd0c048681f5e05356e479c50be4f2c1a7089103f1621d5"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8f78398e67a7227aefa95f876481485403eb974b29e9dc38b307bb6eb2315ea"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef07a0a1d254eeb16455d839cef6e8c2ed127f47f014bbda64a58b5482b6c836"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8124101e92c56827bebef084ff106e8ea11c743256149a95b9fd860d3a4f331f"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08ce9c95a0b093b7aec75676b356a27879901488abc27e9d029273d280438505"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b02dd77a2de6e49078c8937aadabe933ceac04b41c5dde5eca13a69f3cf144e"}, - {file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4dd02e29c8cbed21a1875330b07246b71121a1c08e29f0ee3db5b4cfe16980c4"}, - {file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9c7042488165f7251dc7894cd533a875d2875af6d3b0e09eda9c4b334627ad1c"}, - {file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f809a17cc78bd331e137caa25262b507225854073fd319e987bd216bed911b7c"}, - {file = "rpds_py-0.19.1-cp312-none-win32.whl", hash = "sha256:3ddab996807c6b4227967fe1587febade4e48ac47bb0e2d3e7858bc621b1cace"}, - {file = "rpds_py-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:32e0db3d6e4f45601b58e4ac75c6f24afbf99818c647cc2066f3e4b192dabb1f"}, - {file = "rpds_py-0.19.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:747251e428406b05fc86fee3904ee19550c4d2d19258cef274e2151f31ae9d38"}, - {file = "rpds_py-0.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dc733d35f861f8d78abfaf54035461e10423422999b360966bf1c443cbc42705"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbda75f245caecff8faa7e32ee94dfaa8312a3367397975527f29654cd17a6ed"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd04d8cab16cab5b0a9ffc7d10f0779cf1120ab16c3925404428f74a0a43205a"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2d66eb41ffca6cc3c91d8387509d27ba73ad28371ef90255c50cb51f8953301"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdf4890cda3b59170009d012fca3294c00140e7f2abe1910e6a730809d0f3f9b"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1fa67ef839bad3815124f5f57e48cd50ff392f4911a9f3cf449d66fa3df62a5"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b82c9514c6d74b89a370c4060bdb80d2299bc6857e462e4a215b4ef7aa7b090e"}, - {file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c7b07959866a6afb019abb9564d8a55046feb7a84506c74a6f197cbcdf8a208e"}, - {file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4f580ae79d0b861dfd912494ab9d477bea535bfb4756a2269130b6607a21802e"}, - {file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c6d20c8896c00775e6f62d8373aba32956aa0b850d02b5ec493f486c88e12859"}, - {file = "rpds_py-0.19.1-cp313-none-win32.whl", hash = "sha256:afedc35fe4b9e30ab240b208bb9dc8938cb4afe9187589e8d8d085e1aacb8309"}, - {file = "rpds_py-0.19.1-cp313-none-win_amd64.whl", hash = "sha256:1d4af2eb520d759f48f1073ad3caef997d1bfd910dc34e41261a595d3f038a94"}, - {file = "rpds_py-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:34bca66e2e3eabc8a19e9afe0d3e77789733c702c7c43cd008e953d5d1463fde"}, - {file = "rpds_py-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:24f8ae92c7fae7c28d0fae9b52829235df83f34847aa8160a47eb229d9666c7b"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71157f9db7f6bc6599a852852f3389343bea34315b4e6f109e5cbc97c1fb2963"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d494887d40dc4dd0d5a71e9d07324e5c09c4383d93942d391727e7a40ff810b"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3661e6d4ba63a094138032c1356d557de5b3ea6fd3cca62a195f623e381c76"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97fbb77eaeb97591efdc654b8b5f3ccc066406ccfb3175b41382f221ecc216e8"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cc4bc73e53af8e7a42c8fd7923bbe35babacfa7394ae9240b3430b5dcf16b2a"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:35af5e4d5448fa179fd7fff0bba0fba51f876cd55212f96c8bbcecc5c684ae5c"}, - {file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3511f6baf8438326e351097cecd137eb45c5f019944fe0fd0ae2fea2fd26be39"}, - {file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:57863d16187995c10fe9cf911b897ed443ac68189179541734502353af33e693"}, - {file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9e318e6786b1e750a62f90c6f7fa8b542102bdcf97c7c4de2a48b50b61bd36ec"}, - {file = "rpds_py-0.19.1-cp38-none-win32.whl", hash = "sha256:53dbc35808c6faa2ce3e48571f8f74ef70802218554884787b86a30947842a14"}, - {file = "rpds_py-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:8df1c283e57c9cb4d271fdc1875f4a58a143a2d1698eb0d6b7c0d7d5f49c53a1"}, - {file = "rpds_py-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e76c902d229a3aa9d5ceb813e1cbcc69bf5bda44c80d574ff1ac1fa3136dea71"}, - {file = "rpds_py-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de1f7cd5b6b351e1afd7568bdab94934d656abe273d66cda0ceea43bbc02a0c2"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24fc5a84777cb61692d17988989690d6f34f7f95968ac81398d67c0d0994a897"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:74129d5ffc4cde992d89d345f7f7d6758320e5d44a369d74d83493429dad2de5"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e360188b72f8080fefa3adfdcf3618604cc8173651c9754f189fece068d2a45"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13e6d4840897d4e4e6b2aa1443e3a8eca92b0402182aafc5f4ca1f5e24f9270a"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f09529d2332264a902688031a83c19de8fda5eb5881e44233286b9c9ec91856d"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0d4b52811dcbc1aba08fd88d475f75b4f6db0984ba12275d9bed1a04b2cae9b5"}, - {file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd635c2c4043222d80d80ca1ac4530a633102a9f2ad12252183bcf338c1b9474"}, - {file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f35b34a5184d5e0cc360b61664c1c06e866aab077b5a7c538a3e20c8fcdbf90b"}, - {file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d4ec0046facab83012d821b33cead742a35b54575c4edfb7ed7445f63441835f"}, - {file = "rpds_py-0.19.1-cp39-none-win32.whl", hash = "sha256:f5b8353ea1a4d7dfb59a7f45c04df66ecfd363bb5b35f33b11ea579111d4655f"}, - {file = "rpds_py-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:1fb93d3486f793d54a094e2bfd9cd97031f63fcb5bc18faeb3dd4b49a1c06523"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7d5c7e32f3ee42f77d8ff1a10384b5cdcc2d37035e2e3320ded909aa192d32c3"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:89cc8921a4a5028d6dd388c399fcd2eef232e7040345af3d5b16c04b91cf3c7e"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca34e913d27401bda2a6f390d0614049f5a95b3b11cd8eff80fe4ec340a1208"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5953391af1405f968eb5701ebbb577ebc5ced8d0041406f9052638bafe52209d"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:840e18c38098221ea6201f091fc5d4de6128961d2930fbbc96806fb43f69aec1"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d8b735c4d162dc7d86a9cf3d717f14b6c73637a1f9cd57fe7e61002d9cb1972"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce757c7c90d35719b38fa3d4ca55654a76a40716ee299b0865f2de21c146801c"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9421b23c85f361a133aa7c5e8ec757668f70343f4ed8fdb5a4a14abd5437244"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3b823be829407393d84ee56dc849dbe3b31b6a326f388e171555b262e8456cc1"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:5e58b61dcbb483a442c6239c3836696b79f2cd8e7eec11e12155d3f6f2d886d1"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39d67896f7235b2c886fb1ee77b1491b77049dcef6fbf0f401e7b4cbed86bbd4"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8b32cd4ab6db50c875001ba4f5a6b30c0f42151aa1fbf9c2e7e3674893fb1dc4"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c32e41de995f39b6b315d66c27dea3ef7f7c937c06caab4c6a79a5e09e2c415"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a129c02b42d46758c87faeea21a9f574e1c858b9f358b6dd0bbd71d17713175"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:346557f5b1d8fd9966059b7a748fd79ac59f5752cd0e9498d6a40e3ac1c1875f"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31e450840f2f27699d014cfc8865cc747184286b26d945bcea6042bb6aa4d26e"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01227f8b3e6c8961490d869aa65c99653df80d2f0a7fde8c64ebddab2b9b02fd"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69084fd29bfeff14816666c93a466e85414fe6b7d236cfc108a9c11afa6f7301"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d2b88efe65544a7d5121b0c3b003ebba92bfede2ea3577ce548b69c5235185"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ea961a674172ed2235d990d7edf85d15d8dfa23ab8575e48306371c070cda67"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:5beffdbe766cfe4fb04f30644d822a1080b5359df7db3a63d30fa928375b2720"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:720f3108fb1bfa32e51db58b832898372eb5891e8472a8093008010911e324c5"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c2087dbb76a87ec2c619253e021e4fb20d1a72580feeaa6892b0b3d955175a71"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ddd50f18ebc05ec29a0d9271e9dbe93997536da3546677f8ca00b76d477680c"}, - {file = "rpds_py-0.19.1.tar.gz", hash = "sha256:31dd5794837f00b46f4096aa8ccaa5972f73a938982e32ed817bb520c465e520"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, + {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, + {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, + {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, + {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, + {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, + {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, + {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, + {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, + {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, + {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, + {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, + {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, + {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, +] + +[[package]] +name = "ruff" +version = "0.6.3" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"}, + {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"}, + {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"}, + {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"}, + {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"}, + {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"}, + {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"}, ] [[package]] @@ -3577,19 +3700,23 @@ win32 = ["pywin32"] [[package]] name = "setuptools" -version = "72.1.0" +version = "74.1.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, - {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, + {file = "setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308"}, + {file = "setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"}, ] [package.extras] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] [[package]] name = "six" @@ -3626,13 +3753,13 @@ files = [ [[package]] name = "soupsieve" -version = "2.5" +version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, ] [[package]] @@ -3877,21 +4004,18 @@ develop = ["colorama", "cython (>=0.29.26)", "cython (>=0.29.28,<3.0.0)", "flake docs = ["ipykernel", "jupyter-client", "matplotlib", "nbconvert", "nbformat", "numpydoc", "pandas-datareader", "sphinx"] [[package]] -name = "sympy" -version = "1.13.1" -description = "Computer algebra system (CAS) in Python" -optional = true +name = "stevedore" +version = "5.3.0" +description = "Manage dynamic plugins for Python applications" +optional = false python-versions = ">=3.8" files = [ - {file = "sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8"}, - {file = "sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f"}, + {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, + {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, ] [package.dependencies] -mpmath = ">=1.1.0,<1.4" - -[package.extras] -dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] +pbr = ">=2.0.0" [[package]] name = "terminado" @@ -3965,45 +4089,6 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[[package]] -name = "torch" -version = "2.0.1" -description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" -optional = true -python-versions = ">=3.8.0" -files = [ - {file = "torch-2.0.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:8ced00b3ba471856b993822508f77c98f48a458623596a4c43136158781e306a"}, - {file = "torch-2.0.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:359bfaad94d1cda02ab775dc1cc386d585712329bb47b8741607ef6ef4950747"}, - {file = "torch-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:7c84e44d9002182edd859f3400deaa7410f5ec948a519cc7ef512c2f9b34d2c4"}, - {file = "torch-2.0.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:567f84d657edc5582d716900543e6e62353dbe275e61cdc36eda4929e46df9e7"}, - {file = "torch-2.0.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:787b5a78aa7917465e9b96399b883920c88a08f4eb63b5a5d2d1a16e27d2f89b"}, - {file = "torch-2.0.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:e617b1d0abaf6ced02dbb9486803abfef0d581609b09641b34fa315c9c40766d"}, - {file = "torch-2.0.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b6019b1de4978e96daa21d6a3ebb41e88a0b474898fe251fd96189587408873e"}, - {file = "torch-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:dbd68cbd1cd9da32fe5d294dd3411509b3d841baecb780b38b3b7b06c7754434"}, - {file = "torch-2.0.1-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:ef654427d91600129864644e35deea761fb1fe131710180b952a6f2e2207075e"}, - {file = "torch-2.0.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:25aa43ca80dcdf32f13da04c503ec7afdf8e77e3a0183dd85cd3e53b2842e527"}, - {file = "torch-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5ef3ea3d25441d3957348f7e99c7824d33798258a2bf5f0f0277cbcadad2e20d"}, - {file = "torch-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0882243755ff28895e8e6dc6bc26ebcf5aa0911ed81b2a12f241fc4b09075b13"}, - {file = "torch-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:f66aa6b9580a22b04d0af54fcd042f52406a8479e2b6a550e3d9f95963e168c8"}, - {file = "torch-2.0.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:1adb60d369f2650cac8e9a95b1d5758e25d526a34808f7448d0bd599e4ae9072"}, - {file = "torch-2.0.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:1bcffc16b89e296826b33b98db5166f990e3b72654a2b90673e817b16c50e32b"}, - {file = "torch-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e10e1597f2175365285db1b24019eb6f04d53dcd626c735fc502f1e8b6be9875"}, - {file = "torch-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:423e0ae257b756bb45a4b49072046772d1ad0c592265c5080070e0767da4e490"}, - {file = "torch-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8742bdc62946c93f75ff92da00e3803216c6cce9b132fbca69664ca38cfb3e18"}, - {file = "torch-2.0.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:c62df99352bd6ee5a5a8d1832452110435d178b5164de450831a3a8cc14dc680"}, - {file = "torch-2.0.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:671a2565e3f63b8fe8e42ae3e36ad249fe5e567435ea27b94edaa672a7d0c416"}, -] - -[package.dependencies] -filelock = "*" -jinja2 = "*" -networkx = "*" -sympy = "*" -typing-extensions = "*" - -[package.extras] -opt-einsum = ["opt-einsum (>=3.3)"] - [[package]] name = "tornado" version = "6.4.1" @@ -4026,13 +4111,13 @@ files = [ [[package]] name = "tqdm" -version = "4.66.4" +version = "4.66.5" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, - {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, + {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, + {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, ] [package.dependencies] @@ -4133,24 +4218,24 @@ files = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20240316" +version = "2.9.0.20240821" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, - {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, + {file = "types-python-dateutil-2.9.0.20240821.tar.gz", hash = "sha256:9649d1dcb6fef1046fb18bebe9ea2aa0028b160918518c34589a46045f6ebd98"}, + {file = "types_python_dateutil-2.9.0.20240821-py3-none-any.whl", hash = "sha256:f5889fcb4e63ed4aaa379b44f93c32593d50b9a94c9a60a0c854d8cc3511cd57"}, ] [[package]] name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -4228,13 +4313,13 @@ files = [ [[package]] name = "webcolors" -version = "24.6.0" +version = "24.8.0" description = "A library for working with the color formats defined by HTML and CSS." optional = false python-versions = ">=3.8" files = [ - {file = "webcolors-24.6.0-py3-none-any.whl", hash = "sha256:8cf5bc7e28defd1d48b9e83d5fc30741328305a8195c29a8e668fa45586568a1"}, - {file = "webcolors-24.6.0.tar.gz", hash = "sha256:1d160d1de46b3e81e58d0a280d0c78b467dc80f47294b91b1ad8029d2cedb55b"}, + {file = "webcolors-24.8.0-py3-none-any.whl", hash = "sha256:fc4c3b59358ada164552084a8ebee637c221e4059267d0f8325b3b560f6c7f0a"}, + {file = "webcolors-24.8.0.tar.gz", hash = "sha256:08b07af286a01bcd30d583a7acadf629583d1f79bfef27dd2c2c5c263817277d"}, ] [package.extras] @@ -4284,36 +4369,40 @@ test = ["pytest (>=3.0.0)", "pytest-cov"] [[package]] name = "widgetsnbextension" -version = "4.0.11" +version = "4.0.13" description = "Jupyter interactive widgets for Jupyter Notebook" optional = false python-versions = ">=3.7" files = [ - {file = "widgetsnbextension-4.0.11-py3-none-any.whl", hash = "sha256:55d4d6949d100e0d08b94948a42efc3ed6dfdc0e9468b2c4b128c9a2ce3a7a36"}, - {file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"}, + {file = "widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71"}, + {file = "widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6"}, ] [[package]] name = "zipp" -version = "3.19.2" +version = "3.20.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, + {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, + {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, ] [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [extras] docs = ["sphinx-markdown-tables"] -pytorch = ["torch"] +pytorch = [] tests = ["typed-ast"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<3.12" -content-hash = "733fc1447b577686ebdcbc4402cd1732bfa039be1e3455f8222c4a8ebe8eb240" +content-hash = "41b932cf4db0e558c395c10a4eda7b6aee6cad4a9414b4e69879ba6f8047b3ac" diff --git a/pyproject.toml b/pyproject.toml index 6b0268c0..3538c826 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,5 @@ +# PACKAGE + [tool.poetry] name = "qolmat" version = "0.1.8" @@ -29,11 +31,12 @@ classifiers = [ "Programming Language :: Python :: 3.10", ] +# DEPENDENCIES + [tool.poetry.dependencies] python = ">=3.8.1,<3.12" bump2version = "1.0.1" dcor = "0.6" -ipykernel = "6.21.0" jupyter = "1.0.0" jupyterlab = "1.2.6" jupytext = "1.14.4" @@ -46,27 +49,31 @@ scikit-learn = "1.3.2" sphinx-markdown-tables = { version = "*", optional = true } statsmodels = "0.14.0" typed-ast = { version = "*", optional = true } -torch = { version = "2.0.1", optional = true } twine = "3.7.1" wheel = "0.37.1" +category-encoders = "^2.6.3" +ipykernel = "^6.29.5" [tool.poetry.dev-dependencies] -flake8 = "6.0.0" matplotlib = "3.6.2" -mypy = "1.1.1" pre-commit = "2.21.0" -[tool.poetry.group.test.dependencies] +[tool.poetry.group.checkers.dependencies] +bandit = "^1.7.9" +mypy = "1.1.1" +ruff = "^0.6.3" pytest = "7.2.0" pytest-cov = "4.0.0" pytest-mock = "3.10.0" +[tool.poetry.group.ci.dependencies] +codecov = "^2.1.13" + [tool.poetry.group.docs.dependencies] numpydoc = "1.1.0" sphinx = "4.3.2" sphinx-gallery = "0.10.1" sphinx_rtd_theme = "1.0.0" -typing_extensions = "4.0.1" [tool.poetry.extras] tests = ["typed-ast"] @@ -77,6 +84,43 @@ pytorch = ["torch"] "Bug Tracker" = "https://github.com/Quantmetry/qolmat" "Source Code" = "https://github.com/Quantmetry/qolmat" +[[tool.poetry.source]] +name = "pytorch_cpu" +url = "https://download.pytorch.org/whl/cpu" +priority = "explicit" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + + +# CONFIGURATION +[tool.bandit] +targets = ["qolmat"] + +[tool.mypy] +pretty = true +strict = false +python_version = ">=3.8.1,<3.12" +ignore_missing_imports = true + +[tool.ruff] +line-length = 79 +fix = true +indent-width = 4 +target-version = "py310" +exclude = ["examples/", "docs/"] + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = ["C", "D", "E", "F", "I", "Q", "W"] +ignore = ["C901", "D107"] + +[tool.ruff.lint.isort] +known-first-party = ["qolmat"] + +[tool.ruff.lint.per-file-ignores] +"tests/**/*.py" = ["D100", "D103"] +"__init__.py" = ["D104"] diff --git a/qolmat/analysis/holes_characterization.py b/qolmat/analysis/holes_characterization.py index 5669ac7b..aa511052 100644 --- a/qolmat/analysis/holes_characterization.py +++ b/qolmat/analysis/holes_characterization.py @@ -1,3 +1,5 @@ +"""Script for characterising the holes.""" + from abc import ABC, abstractmethod from typing import Optional, Union @@ -9,34 +11,37 @@ class McarTest(ABC): - """ - Astract class for MCAR tests. - """ + """Astract class for MCAR tests.""" @abstractmethod def test(self, df: pd.DataFrame) -> float: + """Test function.""" pass class LittleTest(McarTest): - """ - This class implements the Little's test, which is designed to detect the heterogeneity accross - the missing patterns. The null hypothesis is "The missing data mechanism is MCAR". The - shortcoming of this test is that it won't detect the heterogeneity of covariance. + """Little Test class. + + This class implements the Little's test, which is designed to detect the + heterogeneity accross the missing patterns. The null hypothesis is + "The missing data mechanism is MCAR". The shortcoming of this test is + that it won't detect the heterogeneity of covariance. References ---------- - Little. "A Test of Missing Completely at Random for Multivariate Data with Missing Values." - Journal of the American Statistical Association, Volume 83, 1988 - Issue 404 + Little. "A Test of Missing Completely at Random for Multivariate Data with + Missing Values." Journal of the American Statistical Association, + Volume 83, 1988 - Issue 404 Parameters ---------- imputer : Optional[ImputerEM] - Imputer based on the EM algorithm. The 'model' attribute must be equal to 'multinormal'. - If None, the default ImputerEM is taken. + Imputer based on the EM algorithm. The 'model' attribute must be + equal to 'multinormal'. If None, the default ImputerEM is taken. random_state : int, RandomState instance or None, default=None Controls the randomness. Pass an int for reproducible output across multiple function calls. + """ def __init__( @@ -47,15 +52,14 @@ def __init__( super().__init__() if imputer and imputer.model != "multinormal": raise AttributeError( - "The ImputerEM model must be 'multinormal' to use the Little's test" + "The ImputerEM model must be 'multinormal' " + "to use the Little's test" ) self.imputer = imputer self.random_state = random_state def test(self, df: pd.DataFrame) -> float: - """ - Apply the Little's test over a real dataframe. - + """Apply the Little's test over a real dataframe. Parameters ---------- @@ -66,6 +70,7 @@ def test(self, df: pd.DataFrame) -> float: ------- float The p-value of the test. + """ imputer = self.imputer or ImputerEM(random_state=self.random_state) imputer = imputer._fit_element(df) @@ -79,16 +84,22 @@ def test(self, df: pd.DataFrame) -> float: # Iterate over the patterns df_nan = df.notna() - for tup_pattern, df_nan_pattern in df_nan.groupby(df_nan.columns.tolist()): + for tup_pattern, df_nan_pattern in df_nan.groupby( + df_nan.columns.tolist() + ): n_rows_pattern, _ = df_nan_pattern.shape ind_pattern = df_nan_pattern.index df_pattern = df.loc[ind_pattern, list(tup_pattern)] obs_mean = df_pattern.mean().to_numpy() diff_means = obs_mean - ml_means[list(tup_pattern)] - inv_sigma_pattern = np.linalg.inv(ml_cov[:, tup_pattern][tup_pattern, :]) + inv_sigma_pattern = np.linalg.inv( + ml_cov[:, tup_pattern][tup_pattern, :] + ) - d0 += n_rows_pattern * np.dot(np.dot(diff_means, inv_sigma_pattern), diff_means.T) + d0 += n_rows_pattern * np.dot( + np.dot(diff_means, inv_sigma_pattern), diff_means.T + ) degree_f += tup_pattern.count(True) return 1 - float(chi2.cdf(d0, degree_f)) diff --git a/qolmat/benchmark/comparator.py b/qolmat/benchmark/comparator.py index 5a60c6f5..4fed2e9e 100644 --- a/qolmat/benchmark/comparator.py +++ b/qolmat/benchmark/comparator.py @@ -1,3 +1,5 @@ +"""Script for comparator.""" + from typing import Any, Dict, List, Optional import numpy as np @@ -8,23 +10,28 @@ class Comparator: - """ - This class implements a comparator for evaluating different imputation methods. + """Comparator class. + + This class implements a comparator for evaluating different + imputation methods. Parameters ---------- dict_models: Dict[str, any] dictionary of imputation methods selected_columns: List[str]Œ - list of column's names selected (all with at least one null value will be imputed) + list of column's names selected (all with at least one null value will + be imputed) columnwise_evaluation : Optional[bool], optional - whether the metric should be calculated column-wise or not, by default False - dict_config_opti: Optional[Dict[str, Dict[str, Union[str, float, int]]]] = {} - dictionary of search space for each implementation method. By default, the value is set to - {}. + whether the metric should be calculated column-wise or not, + by default False + dict_config_opti: Optional[Dict[str, Dict[str, Union[str, float, int]]]] + dictionary of search space for each implementation method. + By default, the value is set to {}. max_evals: int = 10 number of calls of the optimization algorithm 10. + """ def __init__( @@ -53,24 +60,29 @@ def get_errors( df_imputed: pd.DataFrame, df_mask: pd.DataFrame, ) -> pd.DataFrame: - """Functions evaluating the reconstruction's quality + """Get errors - estimate the reconstruction's quality. Parameters ---------- - signal_ref : pd.DataFrame + df_origin : pd.DataFrame reference/orginal signal - signal_imputed : pd.DataFrame + df_imputed : pd.DataFrame imputed signal + df_mask : pd.DataFrame + masked dataframe (NA) Returns ------- pd.DataFrame DataFrame of results obtained via different metrics + """ dict_errors = {} for name_metric in self.metrics: fun_metric = metrics.get_metric(name_metric) - dict_errors[name_metric] = fun_metric(df_origin, df_imputed, df_mask) + dict_errors[name_metric] = fun_metric( + df_origin, df_imputed, df_mask + ) df_errors = pd.concat(dict_errors.values(), keys=dict_errors.keys()) return df_errors @@ -81,23 +93,25 @@ def evaluate_errors_sample( dict_config_opti_imputer: Dict[str, Any] = {}, metric_optim: str = "mse", ) -> pd.Series: - """Evaluate the errors in the cross-validation + """Evaluate the errors in the cross-validation. Parameters ---------- - tested_model : any + imputer : Any imputation model df : pd.DataFrame dataframe to impute dict_config_opti_imputer : Dict search space for tested_model's hyperparameters metric_optim : str - Loss function used when imputers undergo hyperparameter optimization + Loss function used when imputers undergo hyperparameter + optimization Returns ------- pd.Series Series with the errors for each metric and each variable + """ list_errors = [] df_origin = df[self.selected_columns].copy() @@ -117,9 +131,12 @@ def evaluate_errors_sample( subset = self.generator_holes.subset if subset is None: raise ValueError( - "HoleGenerator `subset` should be overwritten in split but it is none!" + "HoleGenerator `subset` should be overwritten in split " + "but it is none!" ) - df_errors = self.get_errors(df_origin[subset], df_imputed[subset], df_mask[subset]) + df_errors = self.get_errors( + df_origin[subset], df_imputed[subset], df_mask[subset] + ) list_errors.append(df_errors) df_errors = pd.DataFrame(list_errors) errors_mean = df_errors.mean(axis=0) @@ -130,20 +147,20 @@ def compare( self, df: pd.DataFrame, ): - """Function to compare different imputation methods on dataframe df + """Compure different imputation methods on dataframe df. Parameters ---------- df : pd.DataFrame - verbose : bool, optional - _description_, by default True + input dataframe (for comparison) + Returns ------- pd.DataFrame - Dataframe with the metrics results, imputers are in columns and indices represent - metrics and variables. - """ + Dataframe with the metrics results, imputers are in columns + and indices represent metrics and variables. + """ dict_errors = {} for name, imputer in self.dict_imputers.items(): @@ -156,7 +173,10 @@ def compare( ) print("done.") except Exception as excp: - print(f"Error while testing {name} of type {type(imputer).__name__}!") + print( + f"Error while testing {name} of type " + f"{type(imputer).__name__}!" + ) raise excp df_errors = pd.DataFrame(dict_errors) diff --git a/qolmat/benchmark/hyperparameters.py b/qolmat/benchmark/hyperparameters.py index eaf6efc4..7aa4a24b 100644 --- a/qolmat/benchmark/hyperparameters.py +++ b/qolmat/benchmark/hyperparameters.py @@ -1,15 +1,15 @@ -import copy -from typing import Any, Callable, Dict, List, Union +"""Script for hyperparameter optimisation.""" -import numpy as np -import pandas as pd +import copy +from typing import Callable, Dict, List # import skopt # from skopt.space import Categorical, Dimension, Integer, Real import hyperopt as ho -from hyperopt.pyll.base import Apply as hoApply -from qolmat.benchmark import metrics +import numpy as np +import pandas as pd +from qolmat.benchmark import metrics from qolmat.benchmark.missing_patterns import _HoleGenerator from qolmat.imputations.imputers import _Imputer from qolmat.utils.utils import HyperValue @@ -22,18 +22,21 @@ def get_objective( metric: str, names_hyperparams: List[str], ) -> Callable: - """ - Define the objective function, which is the average metric computed over the folds provided by + """Define the objective function. + + This is the average metric computed over the folds provided by the hole generator, using a cross-validation. Parameters ---------- imputer: _Imputer - Imputer that should be optimized, it should at least have a fit_transform method and an - imputer_params attribute + Imputer that should be optimized, it should at least have a + fit_transform method and an imputer_params attribute + df : pd.DataFrame + input dataframe generator: _HoleGenerator - Generator creating the masked values in the nested cross validation allowing to measure the - imputer performance + Generator creating the masked values in the nested cross validation + allowing to measure the imputer performance metric: str Metric used as perfomance indicator, common values are `mse` and `mae` names_hyperparams: List[str] @@ -43,6 +46,7 @@ def get_objective( ------- Callable[List[HyperValue], float] Objective function + """ def fun_obf(args: List[HyperValue]) -> float: @@ -58,7 +62,9 @@ def fun_obf(args: List[HyperValue]) -> float: df_imputed = imputer.fit_transform(df_corrupted) subset = generator.subset fun_metric = metrics.get_metric(metric) - errors = fun_metric(df_origin[subset], df_imputed[subset], df_mask[subset]) + errors = fun_metric( + df_origin[subset], df_imputed[subset], df_mask[subset] + ) list_errors.append(errors) mean_errors = np.mean(errors) @@ -76,44 +82,55 @@ def optimize( max_evals: int = 100, verbose: bool = False, ): - """Return the provided imputer with hyperparameters optimized in the provided range in order to - minimize the provided metric. + """Optimisation function. + + Return the provided imputer with hyperparameters optimized in the provided + range in order to minimize the provided metric. Parameters ---------- imputer: _Imputer - Imputer that should be optimized, it should at least have a fit_transform method and an - imputer_params attribute + Imputer that should be optimized, it should at least have a + fit_transform method and an imputer_params attribute + df : pd.DataFrame + input dataframe generator: _HoleGenerator - Generator creating the masked values in the nested cross validation allowing to measure the - imputer performance + Generator creating the masked values in the nested cross validation + allowing to measure the imputer performance metric: str Metric used as perfomance indicator, common values are `mse` and `mae` dict_config: Dict[str, HyperValue] Search space for the tested hyperparameters max_evals: int - Maximum number of evaluation of the performance of the algorithm. Each estimation involves - one call to fit_transform per fold returned by the generator. See the n_fold attribute. + Maximum number of evaluation of the performance of the algorithm. + Each estimation involves one call to fit_transform per fold returned + by the generator. See the n_fold attribute. verbose: bool - Verbosity switch, usefull for imputers that can have unstable behavior for some - hyperparameters values + Verbosity switch, usefull for imputers that can have unstable + behavior for some hyperparameters values Returns ------- _Imputer Optimized imputer + """ imputer = copy.deepcopy(imputer) if dict_config == {}: return imputer names_hyperparams = list(dict_config.keys()) values_hyperparams = list(dict_config.values()) - imputer.imputer_params = tuple(set(imputer.imputer_params) | set(dict_config.keys())) + imputer.imputer_params = tuple( + set(imputer.imputer_params) | set(dict_config.keys()) + ) if verbose and hasattr(imputer, "verbose"): setattr(imputer, "verbose", False) fun_obj = get_objective(imputer, df, generator, metric, names_hyperparams) hyperparams = ho.fmin( - fn=fun_obj, space=values_hyperparams, algo=ho.tpe.suggest, max_evals=max_evals + fn=fun_obj, + space=values_hyperparams, + algo=ho.tpe.suggest, + max_evals=max_evals, ) for key, value in hyperparams.items(): diff --git a/qolmat/benchmark/metrics.py b/qolmat/benchmark/metrics.py index f8f87441..b8af8667 100644 --- a/qolmat/benchmark/metrics.py +++ b/qolmat/benchmark/metrics.py @@ -1,15 +1,17 @@ +"""Script for metrics.""" + from functools import partial from typing import Callable, Dict, List +import dcor import numpy as np import pandas as pd import scipy +from numpy.linalg import LinAlgError from sklearn import metrics as skm -import dcor from qolmat.utils import algebra, utils from qolmat.utils.exceptions import NotEnoughSamples -from numpy.linalg import LinAlgError EPS = np.finfo(float).eps @@ -26,7 +28,9 @@ def columnwise_metric( type_cols: str = "all", **kwargs, ) -> pd.Series: - """For each column, compute a metric score based on the true dataframe + """Compute column-wise metrics. + + For each column, compute a metric score based on the true dataframe and the predicted dataframe Parameters @@ -44,17 +48,21 @@ def columnwise_metric( - `all` to apply the metric to all columns - `numerical` to apply the metric to numerical columns only - `categorical` to apply the metric to categorical columns only + **kwargs: dict + additional arguments Returns ------- pd.Series Series of scores for all columns + """ try: pd.testing.assert_index_equal(df1.columns, df2.columns) except AssertionError: raise ValueError( - f"Input dataframes do not have the same columns! ({df1.columns} != {df2.columns})" + "Input dataframes do not have the same columns! " + f"({df1.columns} != {df2.columns})" ) if type_cols == "all": cols = df1.columns @@ -63,19 +71,23 @@ def columnwise_metric( elif type_cols == "categorical": cols = utils._get_categorical_features(df1) else: - raise ValueError(f"Value {type_cols} is not valid for parameter `type_cols`!") + raise ValueError( + f"Value {type_cols} is not valid for parameter `type_cols`!" + ) values = {} for col in cols: df1_col = df1.loc[df_mask[col], col] df2_col = df2.loc[df_mask[col], col] - assert df1_col.notna().all() - assert df2_col.notna().all() + if df1_col.isna().any() or df2_col.isna().any(): + raise ValueError(f"Column {col} contains NaN.") values[col] = metric(df1_col, df2_col, **kwargs) return pd.Series(values) -def mean_squared_error(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> pd.Series: +def mean_squared_error( + df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame +) -> pd.Series: """Mean squared error between two dataframes. Parameters @@ -90,14 +102,17 @@ def mean_squared_error(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFra Returns ------- pd.Series + """ - return columnwise_metric(df1, df2, df_mask, skm.mean_squared_error, type_cols="numerical") + return columnwise_metric( + df1, df2, df_mask, skm.mean_squared_error, type_cols="numerical" + ) def root_mean_squared_error( df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame ) -> pd.Series: - """Root mean squared error between two dataframes. + """Compute the root mean squared error between two dataframes. Parameters ---------- @@ -111,14 +126,22 @@ def root_mean_squared_error( Returns ------- pd.Series + """ return columnwise_metric( - df1, df2, df_mask, skm.mean_squared_error, type_cols="numerical", squared=False + df1, + df2, + df_mask, + skm.mean_squared_error, + type_cols="numerical", + squared=False, ) -def mean_absolute_error(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> pd.Series: - """Mean absolute error between two dataframes. +def mean_absolute_error( + df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame +) -> pd.Series: + """Compute the mean absolute error between two dataframes. Parameters ---------- @@ -132,14 +155,17 @@ def mean_absolute_error(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFr Returns ------- pd.Series + """ - return columnwise_metric(df1, df2, df_mask, skm.mean_absolute_error, type_cols="numerical") + return columnwise_metric( + df1, df2, df_mask, skm.mean_absolute_error, type_cols="numerical" + ) def mean_absolute_percentage_error( df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame ) -> pd.Series: - """Mean absolute percentage error between two dataframes. + """Compute the mean absolute percentage error between two dataframes. Parameters ---------- @@ -153,14 +179,22 @@ def mean_absolute_percentage_error( Returns ------- pd.Series + """ return columnwise_metric( - df1, df2, df_mask, skm.mean_absolute_percentage_error, type_cols="numerical" + df1, + df2, + df_mask, + skm.mean_absolute_percentage_error, + type_cols="numerical", ) -def _weighted_mean_absolute_percentage_error_1D(values1: pd.Series, values2: pd.Series) -> float: - """Weighted mean absolute percentage error between two series. +def _weighted_mean_absolute_percentage_error_1D( + values1: pd.Series, values2: pd.Series +) -> float: + """Compute the weighted mean absolute perc. error between 2 series. + Based on https://en.wikipedia.org/wiki/Mean_absolute_percentage_error Parameters @@ -174,6 +208,7 @@ def _weighted_mean_absolute_percentage_error_1D(values1: pd.Series, values2: pd. ------- float Weighted mean absolute percentage error + """ return (values1 - values2).abs().sum() / values1.abs().sum() @@ -181,7 +216,7 @@ def _weighted_mean_absolute_percentage_error_1D(values1: pd.Series, values2: pd. def weighted_mean_absolute_percentage_error( df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame ) -> pd.Series: - """Weighted mean absolute percentage error between two dataframes. + """Compute the weighted mean absolute percentage error between 2 df. Parameters ---------- @@ -195,6 +230,7 @@ def weighted_mean_absolute_percentage_error( Returns ------- pd.Series + """ return columnwise_metric( df1, @@ -205,9 +241,10 @@ def weighted_mean_absolute_percentage_error( ) -def accuracy(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> pd.Series: - """ - Matching ratio beetween the two datasets. +def accuracy( + df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame +) -> pd.Series: + """Compute the matching ratio beetween the two datasets. Parameters ---------- @@ -221,6 +258,7 @@ def accuracy(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> pd. Returns ------- pd.Series + """ return columnwise_metric( df1, @@ -232,8 +270,7 @@ def accuracy(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> pd. def accuracy_1D(values1: pd.Series, values2: pd.Series) -> float: - """ - Matching ratio beetween the set of values. + """Compute the matching ratio beetween the set of values. Parameters ---------- @@ -246,6 +283,7 @@ def accuracy_1D(values1: pd.Series, values2: pd.Series) -> float: ------- float accuracy + """ return (values1 == values2).mean() @@ -256,8 +294,9 @@ def dist_wasserstein( df_mask: pd.DataFrame, method: str = "columnwise", ) -> pd.Series: - """Wasserstein distances between columns of 2 dataframes. - Wasserstein distance can only be computed columnwise + """Compute the Wasserstein distances between columns of 2 dataframes. + + Wasserstein distance can only be computed columnwise. Parameters ---------- @@ -267,24 +306,34 @@ def dist_wasserstein( Predicted dataframe df_mask : pd.DataFrame Elements of the dataframes to compute on + method : str, optional + columnwise or not Returns ------- pd.Series wasserstein distances + """ if method == "columnwise": - return columnwise_metric(df1, df2, df_mask, scipy.stats.wasserstein_distance) + return columnwise_metric( + df1, df2, df_mask, scipy.stats.wasserstein_distance + ) else: raise AssertionError( - f"The parameter of the function wasserstein_distance should be one of" - f"the following: [`columnwise`], not `{method}`!" + f"The parameter of the function wasserstein_distance should " + "be one of the following: " + f"[`columnwise`], not `{method}`!" ) def kolmogorov_smirnov_test_1D(df1: pd.Series, df2: pd.Series) -> float: - """Compute KS test statistic of the two-sample Kolmogorov-Smirnov test for goodness of fit. - See more in https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.ks_2samp.html. + """Compute KS test statistic. + + Compute KS test stat. of the two-sample Kolmogorov-Smirnov test + for goodness of fit. + See more in + https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.ks_2samp.html. Parameters ---------- @@ -297,6 +346,7 @@ def kolmogorov_smirnov_test_1D(df1: pd.Series, df2: pd.Series) -> float: ------- float KS test statistic + """ return scipy.stats.ks_2samp(df1, df2)[0] @@ -304,7 +354,8 @@ def kolmogorov_smirnov_test_1D(df1: pd.Series, df2: pd.Series) -> float: def kolmogorov_smirnov_test( df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame ) -> pd.Series: - """Kolmogorov Smirnov Test for numerical features. + """Compute the Kolmogorov Smirnov Test for numerical features. + Lower score means better performance. Parameters @@ -320,12 +371,16 @@ def kolmogorov_smirnov_test( ------- pd.Series KS test statistic + """ - return columnwise_metric(df1, df2, df_mask, kolmogorov_smirnov_test_1D, type_cols="numerical") + return columnwise_metric( + df1, df2, df_mask, kolmogorov_smirnov_test_1D, type_cols="numerical" + ) def _total_variance_distance_1D(df1: pd.Series, df2: pd.Series) -> float: - """Compute Total Variance Distance for a categorical feature + """Compute Total Variance Distance for a categorical feature. + It is based on TVComplement in https://github.com/sdv-dev/SDMetrics Parameters @@ -339,6 +394,7 @@ def _total_variance_distance_1D(df1: pd.Series, df2: pd.Series) -> float: ------- float Total variance distance + """ list_categories = list(set(df1.unique()).union(set(df2.unique()))) freqs1 = df1.value_counts() / len(df1) @@ -351,7 +407,8 @@ def _total_variance_distance_1D(df1: pd.Series, df2: pd.Series) -> float: def total_variance_distance( df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame ) -> pd.Series: - """Total variance distance for categorical features + """Compute the total variance distance for categorical features. + It is based on TVComplement in https://github.com/sdv-dev/SDMetrics Parameters @@ -367,6 +424,7 @@ def total_variance_distance( ------- pd.Series Total variance distance + """ return columnwise_metric( df1, @@ -382,9 +440,13 @@ def _check_same_number_columns(df1: pd.DataFrame, df2: pd.DataFrame): raise Exception("inputs have to have the same number of columns.") -def _get_correlation_pearson_matrix(df: pd.DataFrame, use_p_value: bool = True) -> pd.DataFrame: - """Get matrix of correlation values for numerical features - based on Pearson correlation coefficient or p-value for testing non-correlation. +def _get_correlation_pearson_matrix( + df: pd.DataFrame, use_p_value: bool = True +) -> pd.DataFrame: + """Get matrix of correlation values for numerical features. + + Based on Pearson correlation coefficient or p-value for + testing non-correlation. Parameters ---------- @@ -397,12 +459,15 @@ def _get_correlation_pearson_matrix(df: pd.DataFrame, use_p_value: bool = True) ------- pd.DataFrame Correlation matrix + """ cols = df.columns.tolist() matrix = np.zeros((len(df.columns), len(df.columns))) for idx_1, col_1 in enumerate(cols): for idx_2, col_2 in enumerate(cols): - res = scipy.stats.mstats.pearsonr(df[[col_1]].values, df[[col_2]].values) + res = scipy.stats.mstats.pearsonr( + df[[col_1]].values, df[[col_2]].values + ) if use_p_value: matrix[idx_1, idx_2] = res[1] else: @@ -417,8 +482,11 @@ def mean_difference_correlation_matrix_numerical_features( df_mask: pd.DataFrame, use_p_value: bool = True, ) -> pd.Series: - """Mean absolute of differences between the correlation matrices of df1 and df2. - based on Pearson correlation coefficient or p-value for testing non-correlation. + """Compute the mean absolute of differences. + + Computed between the correlation matrices of df1 and df2. + based on Pearson correlation coefficient or p-value for + testing non-correlation. Parameters ---------- @@ -435,6 +503,7 @@ def mean_difference_correlation_matrix_numerical_features( ------- pd.Series Mean absolute of differences for each feature + """ df1 = df1[df_mask].dropna(axis=0) df2 = df2[df_mask].dropna(axis=0) @@ -442,28 +511,38 @@ def mean_difference_correlation_matrix_numerical_features( _check_same_number_columns(df1, df2) cols_numerical = utils._get_numerical_features(df1) - df_corr1 = _get_correlation_pearson_matrix(df1[cols_numerical], use_p_value=use_p_value) - df_corr2 = _get_correlation_pearson_matrix(df2[cols_numerical], use_p_value=use_p_value) + df_corr1 = _get_correlation_pearson_matrix( + df1[cols_numerical], use_p_value=use_p_value + ) + df_corr2 = _get_correlation_pearson_matrix( + df2[cols_numerical], use_p_value=use_p_value + ) diff_corr = (df_corr1 - df_corr2).abs().mean(axis=1) return pd.Series(diff_corr, index=cols_numerical) -def _get_correlation_chi2_matrix(data: pd.DataFrame, use_p_value: bool = True) -> pd.DataFrame: - """Get matrix of correlation values for categorical features - based on Chi-square test of independence of variables (the test statistic or the p-value). +def _get_correlation_chi2_matrix( + data: pd.DataFrame, use_p_value: bool = True +) -> pd.DataFrame: + """Get matrix of correlation values for categorical features. + + Based on Chi-square test of independence of variables + (the test statistic or the p-value). Parameters ---------- - df : pd.DataFrame + data : pd.DataFrame dataframe use_p_value : bool, optional - use the p-value of the test instead of the test statistic, by default True + use the p-value of the test instead of the test statistic, + by default True Returns ------- pd.DataFrame Correlation matrix + """ cols = data.columns.tolist() matrix = np.zeros((len(data.columns), len(data.columns))) @@ -486,8 +565,11 @@ def mean_difference_correlation_matrix_categorical_features( df_mask: pd.DataFrame, use_p_value: bool = True, ) -> pd.Series: - """Mean absolute of differences between the correlation matrix of df1 and df2 - based on Chi-square test of independence of variables (the test statistic or the p-value) + """Compute the mean absolute of differences. + + Computed between the correlation matrix of df1 and df2 + based on Chi-square test of independence of variables + (the test statistic or the p-value) Parameters ---------- @@ -498,12 +580,14 @@ def mean_difference_correlation_matrix_categorical_features( df_mask : pd.DataFrame Elements of the dataframes to compute on use_p_value : bool, optional - use the p-value of the test instead of the test statistic, by default True + use the p-value of the test instead of the test statistic, + by default True Returns ------- pd.Series Mean absolute of differences for each feature + """ df1 = df1[df_mask].dropna(axis=0) df2 = df2[df_mask].dropna(axis=0) @@ -511,8 +595,12 @@ def mean_difference_correlation_matrix_categorical_features( _check_same_number_columns(df1, df2) cols_categorical = utils._get_categorical_features(df1) - df_corr1 = _get_correlation_chi2_matrix(df1[cols_categorical], use_p_value=use_p_value) - df_corr2 = _get_correlation_chi2_matrix(df2[cols_categorical], use_p_value=use_p_value) + df_corr1 = _get_correlation_chi2_matrix( + df1[cols_categorical], use_p_value=use_p_value + ) + df_corr2 = _get_correlation_chi2_matrix( + df2[cols_categorical], use_p_value=use_p_value + ) diff_corr = (df_corr1 - df_corr2).abs().mean(axis=1) return pd.Series(diff_corr, index=cols_categorical) @@ -524,7 +612,9 @@ def _get_correlation_f_oneway_matrix( cols_numerical: List[str], use_p_value: bool = True, ) -> pd.DataFrame: - """Get matrix of correlation values between categorical and numerical features + """Get matrix of correlation values. + + Computed between categorical and numerical features based on the one-way ANOVA. Parameters @@ -536,12 +626,14 @@ def _get_correlation_f_oneway_matrix( cols_numerical : List[str] list numerical columns use_p_value : bool, optional - use the p-value of the test instead of the test statistic, by default True + use the p-value of the test instead of the test statistic, + by default True Returns ------- pd.DataFrame Correlation matrix + """ matrix = np.zeros((len(cols_categorical), len(cols_numerical))) for idx_cat, col_cat in enumerate(cols_categorical): @@ -561,7 +653,9 @@ def mean_diff_corr_matrix_categorical_vs_numerical_features( df_mask: pd.DataFrame, use_p_value: bool = True, ) -> pd.Series: - """Mean absolute of differences between the correlation matrix of df1 and df2 + """Compute the mean absolute of differences. + + Computation between the correlation matrix of df1 and df2 based on the one-way ANOVA. Parameters @@ -573,12 +667,14 @@ def mean_diff_corr_matrix_categorical_vs_numerical_features( df_mask : pd.DataFrame Elements of the dataframes to compute on use_p_value : bool, optional - use the p-value of the test instead of the test statistic, by default True + use the p-value of the test instead of the test statistic, + by default True Returns ------- pd.Series Mean absolute of differences for each feature + """ df1 = df1[df_mask].dropna(axis=0) df2 = df2[df_mask].dropna(axis=0) @@ -603,7 +699,8 @@ def mean_diff_corr_matrix_categorical_vs_numerical_features( def _sum_manhattan_distances_1D(values: pd.Series) -> float: - """Sum of Manhattan distances computed for one column + """Compute the sum of Manhattan distances computed for one column. + It is based on https://www.geeksforgeeks.org/sum-manhattan-distances-pairs-points/ Parameters @@ -615,6 +712,7 @@ def _sum_manhattan_distances_1D(values: pd.Series) -> float: ------- float Sum of Manhattan distances + """ values = values.sort_values(ascending=True) sums_partial = values.shift().fillna(0.0).cumsum() @@ -624,25 +722,31 @@ def _sum_manhattan_distances_1D(values: pd.Series) -> float: def _sum_manhattan_distances(df1: pd.DataFrame) -> float: - """Sum Manhattan distances between all pairs of rows. + """Compute the sum Manhattan distances between all pairs of rows. + It is based on https://www.geeksforgeeks.org/sum-manhattan-distances-pairs-points/ Parameters ---------- df1 : pd.DataFrame + input dataframe Returns ------- float Sum of Manhattan distances for all pairs of rows. + """ cols = df1.columns.tolist() result = sum([_sum_manhattan_distances_1D(df1[col]) for col in cols]) return result -def sum_energy_distances(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> pd.Series: - """Sum of energy distances between df1 and df2. +def sum_energy_distances( + df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame +) -> pd.Series: + """Compute the sum of energy distances between df1 and df2. + It is based on https://dcor.readthedocs.io/en/latest/theory.html# Parameters @@ -658,8 +762,8 @@ def sum_energy_distances(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataF ------- pd.Series Sum of energy distances between df1 and df2. - """ + """ # Replace nan in dataframe df1 = df1[df_mask].fillna(0.0) df2 = df2[df_mask].fillna(0.0) @@ -670,7 +774,11 @@ def sum_energy_distances(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataF df = pd.concat([df1, df2]) sum_distances_df1_df2 = _sum_manhattan_distances(df) - sum_distance = 2 * sum_distances_df1_df2 - 4 * sum_distances_df1 - 4 * sum_distances_df2 + sum_distance = ( + 2 * sum_distances_df1_df2 + - 4 * sum_distances_df1 + - 4 * sum_distances_df2 + ) return pd.Series(sum_distance, index=["All"]) @@ -681,7 +789,8 @@ def sum_pairwise_distances( df_mask: pd.DataFrame, metric: str = "cityblock", ) -> float: - """Sum of pairwise distances based on a predefined metric. + """Compute the sum of pairwise distances based on a predefined metric. + Metrics are found in this link https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cdist.html @@ -700,6 +809,7 @@ def sum_pairwise_distances( ------- float Sum of pairwise distances based on a predefined metric + """ df1 = df1[df_mask.any(axis=1)] df2 = df2[df_mask.any(axis=1)] @@ -717,12 +827,16 @@ def frechet_distance_base( df1: pd.DataFrame, df2: pd.DataFrame, ) -> pd.Series: - """Compute the Fréchet distance between two dataframes df1 and df2 - Frechet_distance = || mu_1 - mu_2 ||_2^2 + Tr(Sigma_1 + Sigma_2 - 2(Sigma_1 . Sigma_2)^(1/2)) - It is normalized, df1 and df2 are first scaled by a factor (std(df1) + std(df2)) / 2 - and then centered around (mean(df1) + mean(df2)) / 2 - Based on: Dowson, D. C., and BV666017 Landau. "The Fréchet distance between multivariate normal - distributions." Journal of multivariate analysis 12.3 (1982): 450-455. + """Compute the Fréchet distance between two dataframes df1 and df2. + + Frechet_distance = || mu_1 - mu_2 ||_2^2 + + Tr(Sigma_1 + Sigma_2 - 2(Sigma_1 . Sigma_2)^(1/2)) + It is normalized, df1 and df2 are first scaled by a factor + (std(df1) + std(df2)) / 2 and then centered around + (mean(df1) + mean(df2)) / 2 + Based on: Dowson, D. C., and BV666017 Landau. + "The Fréchet distance between multivariate normal distributions." + Journal of multivariate analysis 12.3 (1982): 450-455. Parameters ---------- @@ -735,8 +849,8 @@ def frechet_distance_base( ------- pd.Series Frechet distance in a Series object - """ + """ if df1.shape != df2.shape: raise Exception("inputs have to be of same dimensions.") @@ -759,12 +873,13 @@ def frechet_distance( method: str = "single", min_n_rows: int = 10, ) -> pd.Series: - """ - Frechet distance computed using a pattern decomposition. Several variant are implemented: - - the `single` method relies on a single estimation of the means and covariance matrix. It is - relevent for MCAR data. - - the `pattern`method relies on the aggregation of the estimated distance between each - pattern. It is relevent for MAR data. + """Compute Frechet distance computed using a pattern decomposition. + + Several variant are implemented: + - the `single` method relies on a single estimation of the means and + covariance matrix. It is relevent for MCAR data. + - the `pattern`method relies on the aggregation of the estimated distance + between each pattern. It is relevent for MAR data. Parameters ---------- @@ -775,8 +890,8 @@ def frechet_distance( df_mask : pd.DataFrame Mask indicating on which values the distance has to computed on method: str - Method used to compute the distance on multivariate datasets with missing values. - Possible values are `robust` and `pattern`. + Method used to compute the distance on multivariate datasets with + missing values. Possible values are `robust` and `pattern`. min_n_rows: int Minimum number of rows for a KL estimation @@ -784,8 +899,8 @@ def frechet_distance( ------- pd.Series Series of computed metrics - """ + """ if method == "single": return frechet_distance_base(df1, df2) return pattern_based_weighted_mean_metric( @@ -799,9 +914,12 @@ def frechet_distance( def kl_divergence_1D(df1: pd.Series, df2: pd.Series) -> float: - """Estimation of the Kullback-Leibler divergence between the two 1D empirical distributions - given by `df1`and `df2`. The samples are binarized using a uniform spacing with 20 bins from - the smallest to the largest value. Not that this may be a coarse estimation. + """Estimate the the Kullback-Leibler divergence for 1D. + + Computation between the two 1D empirical distributions + given by `df1`and `df2`. The samples are binarized using a uniform spacing + with 20 bins from the smallest to the largest value. Not that this may be + a coarse estimation. Parameters ---------- @@ -814,6 +932,7 @@ def kl_divergence_1D(df1: pd.Series, df2: pd.Series) -> float: ------- float Kullback-Leibler divergence between the two empirical distributions. + """ min_val = min(df1.min(), df2.min()) max_val = max(df1.max(), df2.max()) @@ -824,7 +943,9 @@ def kl_divergence_1D(df1: pd.Series, df2: pd.Series) -> float: def kl_divergence_gaussian(df1: pd.DataFrame, df2: pd.DataFrame) -> float: - """Kullback-Leibler divergence estimation based on a Gaussian approximation of both empirical + """Compute Kullback-Leibler divergence estimation. + + Computation based on a Gaussian approximation of both empirical distributions Parameters @@ -838,16 +959,20 @@ def kl_divergence_gaussian(df1: pd.DataFrame, df2: pd.DataFrame) -> float: ------- pd.Series Series of estimated metrics + """ cov1 = df1.cov().values cov2 = df2.cov().values means1 = np.array(df1.mean()) means2 = np.array(df2.mean()) try: - div_kl = algebra.kl_divergence_gaussian_exact(means1, cov1, means2, cov2) + div_kl = algebra.kl_divergence_gaussian_exact( + means1, cov1, means2, cov2 + ) except LinAlgError: raise ValueError( - "Provided datasets have degenerate colinearities, KL-divergence cannot be computed!" + "Provided datasets have degenerate colinearities, KL-divergence " + "cannot be computed!" ) return div_kl @@ -859,11 +984,12 @@ def kl_divergence( method: str = "columnwise", min_n_rows: int = 10, ) -> pd.Series: - """ - Estimation of the Kullback-Leibler divergence between too empirical distributions. Three - methods are implemented: - - columnwise, relying on a uniform binarization and only taking marginals into account - (https://en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence), + """Estimate the KL divergence. + + Estimation of the Kullback-Leibler divergence between too empirical + distributions. Three methods are implemented: + - columnwise, relying on a uniform binarization and only taking marginals + into account (https://en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence), - gaussian, relying on a Gaussian approximation, Parameters @@ -875,8 +1001,8 @@ def kl_divergence( df_mask: pd.DataFrame Mask indicating on what values the divergence should be computed method: str - Method used to compute the divergence on multivariate datasets with missing values. - Possible values are `columnwise` and `gaussian`. + Method used to compute the divergence on multivariate datasets with + missing values. Possible values are `columnwise` and `gaussian`. min_n_rows: int Minimum number of rows for a KL estimation @@ -888,11 +1014,15 @@ def kl_divergence( Raises ------ AssertionError - If the empirical distributions do not have enough samples to estimate a KL divergence. - Consider using a larger dataset of lowering the parameter `min_n_rows`. + If the empirical distributions do not have enough samples to estimate + a KL divergence. Consider using a larger dataset of lowering + the parameter `min_n_rows`. + """ if method == "columnwise": - return columnwise_metric(df1, df2, df_mask, kl_divergence_1D, type_cols="numerical") + return columnwise_metric( + df1, df2, df_mask, kl_divergence_1D, type_cols="numerical" + ) elif method == "gaussian": return pattern_based_weighted_mean_metric( df1, @@ -904,13 +1034,17 @@ def kl_divergence( ) else: raise AssertionError( - f"The parameter of the function wasserstein_distance should be one of" - f"the following: [`columnwise`, `gaussian`], not `{method}`!" + f"The parameter of the function wasserstein_distance " + "should be one of the following: " + f"[`columnwise`, `gaussian`], not `{method}`!" ) def distance_anticorr(df1: pd.DataFrame, df2: pd.DataFrame) -> float: - """Score based on the distance anticorrelation between two empirical distributions. + """Compute distance anticorr. + + Score based on the distance anticorrelation between + two empirical distributions. The theoretical basis can be found on dcor documentation: https://dcor.readthedocs.io/en/latest/theory.html @@ -925,6 +1059,7 @@ def distance_anticorr(df1: pd.DataFrame, df2: pd.DataFrame) -> float: ------- float Distance correlation score + """ return (1 - dcor.distance_correlation(df1.values, df2.values)) / 2 @@ -935,7 +1070,7 @@ def distance_anticorr_pattern( df_mask: pd.DataFrame, min_n_rows: int = 10, ) -> pd.Series: - """Correlation distance computed using a pattern decomposition + """Compute correlation distance computed using a pattern decomposition. Parameters ---------- @@ -952,8 +1087,8 @@ def distance_anticorr_pattern( ------- pd.Series Series of computed metrics - """ + """ return pattern_based_weighted_mean_metric( df1, df2, @@ -974,6 +1109,7 @@ def pattern_based_weighted_mean_metric( **kwargs, ) -> pd.Series: """Compute a mean score based on missing patterns. + Note that for each pattern, a score is returned by the function metric. This code is based on https://www.statsmodels.org/ @@ -989,11 +1125,16 @@ def pattern_based_weighted_mean_metric( metric function min_n_rows : int, optional minimum number of row allowed for a pattern without nan, by default 10 + type_cols : str, optional + type of the columns ("all", "numerical", "categorical") + **kwargs : dict + additional arguments Returns ------- pd.Series _description_ + """ if type_cols == "all": cols = df1.columns @@ -1002,7 +1143,9 @@ def pattern_based_weighted_mean_metric( elif type_cols == "categorical": cols = df1.select_dtypes(exclude=["number"]).columns else: - raise ValueError(f"Value {type_cols} is not valid for parameter `type_cols`!") + raise ValueError( + f"Value {type_cols} is not valid for parameter `type_cols`!" + ) if np.any(df_mask & df1.isna()): raise ValueError("The argument df1 has missing values on the mask!") @@ -1016,7 +1159,9 @@ def pattern_based_weighted_mean_metric( df2 = df2[cols].loc[rows_mask] df_mask = df_mask[cols].loc[rows_mask] max_num_row = 0 - for tup_pattern, df_mask_pattern in df_mask.groupby(df_mask.columns.tolist()): + for tup_pattern, df_mask_pattern in df_mask.groupby( + df_mask.columns.tolist() + ): ind_pattern = df_mask_pattern.index df1_pattern = df1.loc[ind_pattern, list(tup_pattern)] max_num_row = max(max_num_row, len(df1_pattern)) @@ -1027,12 +1172,27 @@ def pattern_based_weighted_mean_metric( scores.append(metric(df1_pattern, df2_pattern, **kwargs)) if len(scores) == 0: raise NotEnoughSamples(max_num_row, min_n_rows) - return pd.Series(sum([s * w for s, w in zip(scores, weights)]), index=["All"]) + return pd.Series( + sum([s * w for s, w in zip(scores, weights)]), index=["All"] + ) def get_metric( name: str, ) -> Callable[[pd.DataFrame, pd.DataFrame, pd.DataFrame], pd.Series]: + """Get metric. + + Parameters + ---------- + name : str + name of the metic to compute + + Returns + ------- + Callable[[pd.DataFrame, pd.DataFrame, pd.DataFrame], pd.Series] + metric + + """ dict_metrics: Dict[str, Callable] = { "mse": mean_squared_error, "rmse": root_mean_squared_error, @@ -1043,7 +1203,9 @@ def get_metric( "KL_columnwise": partial(kl_divergence, method="columnwise"), "KL_gaussian": partial(kl_divergence, method="gaussian"), "KS_test": kolmogorov_smirnov_test, - "correlation_diff": mean_difference_correlation_matrix_numerical_features, + "correlation_diff": ( + mean_difference_correlation_matrix_numerical_features + ), "energy": sum_energy_distances, "frechet": partial(frechet_distance, method="single"), "frechet_pattern": partial(frechet_distance, method="pattern"), diff --git a/qolmat/benchmark/missing_patterns.py b/qolmat/benchmark/missing_patterns.py index 65b6d6ea..2f317a4a 100644 --- a/qolmat/benchmark/missing_patterns.py +++ b/qolmat/benchmark/missing_patterns.py @@ -1,19 +1,33 @@ +"""Script for missing patterns.""" + from __future__ import annotations import functools -from typing import Callable, List, Optional, Tuple, Union +import math import warnings +from typing import Callable, List, Optional, Tuple, Union import numpy as np import pandas as pd from sklearn import utils as sku -from sklearn.utils import resample -import math -from qolmat.utils.exceptions import NoMissingValue, SubsetIsAString +from qolmat.utils.exceptions import SubsetIsAString def compute_transition_counts_matrix(states: pd.Series): + """Compute transtion counts matrix. + + Parameters + ---------- + states : pd.Series + possible states (masks) + + Returns + ------- + pd.Series | pd.DataFrame + transition counts matrix + + """ if isinstance(states.iloc[0], tuple): n_variables = len(states.iloc[0]) state_nonan = pd.Series([tuple([False] * n_variables)]) @@ -28,18 +42,48 @@ def compute_transition_counts_matrix(states: pd.Series): return df_counts -def compute_transition_matrix(states: pd.Series, ngroups: Optional[List] = None): +def compute_transition_matrix( + states: pd.Series, ngroups: Optional[List] = None +): + """Compute the transition matrix. + + Parameters + ---------- + states : pd.Series + serie of possible states (masks) + ngroups : Optional[List], optional + groups, by default None + + Returns + ------- + pd.DataFrame | pd.Series + transition matrix + + """ if ngroups is None: df_counts = compute_transition_counts_matrix(states) else: - list_counts = [compute_transition_counts_matrix(df) for _, df in states.groupby(ngroups)] - df_counts = functools.reduce(lambda a, b: a.add(b, fill_value=0), list_counts) + list_counts = [ + compute_transition_counts_matrix(df) + for _, df in states.groupby(ngroups) + ] + df_counts = functools.reduce( + lambda a, b: a.add(b, fill_value=0), list_counts + ) df_transition = df_counts.div(df_counts.sum(axis=1), axis=0) return df_transition def get_sizes_max(values_isna: pd.Series) -> pd.Series[int]: + """Get max sizes. + + Parameters + ---------- + values_isna : pd.Series + pandas series indicating if value is missing. + + """ ids_hole = (values_isna.diff() != 0).cumsum() sizes_max = values_isna.groupby(ids_hole, group_keys=True).apply( lambda x: (~x) * np.arange(len(x)) @@ -51,14 +95,16 @@ def get_sizes_max(values_isna: pd.Series) -> pd.Series[int]: class _HoleGenerator: - """ - This abstract class implements the generic method to generate masks according to law of missing - values. + """Abstract HoleGenerator class. + + This abstract class implements the generic method to generate masks + according to law of missing values. Parameters ---------- n_splits : int - number of dataframes with missing additional missing values to be created + number of dataframes with missing additional missing values to be + created subset : Optional[List[str]] Names of the columns for which holes must be created, by default None ratio_masked : Optional[float] @@ -68,6 +114,7 @@ class _HoleGenerator: Pass an int for reproducible output across multiple function calls. groups: Tuple[str, ...] Column names used to group the data + """ generate_mask: Callable @@ -88,20 +135,22 @@ def __init__( self.groups = groups def fit(self, X: pd.DataFrame) -> _HoleGenerator: - """ - Fits the generator. + """Fit the generator. Parameters ---------- X : pd.DataFrame Initial dataframe with a missing pattern to be imitated. + """ self._check_subset(X) self.dict_ratios = {} missing_per_col = X[self.subset].isna().sum() self.dict_ratios = (missing_per_col / missing_per_col.sum()).to_dict() if self.groups: - self.ngroups = X.groupby(list(self.groups)).ngroup().rename("_ngroup") + self.ngroups = ( + X.groupby(list(self.groups)).ngroup().rename("_ngroup") + ) else: self.ngroups = None @@ -109,6 +158,7 @@ def fit(self, X: pd.DataFrame) -> _HoleGenerator: def split(self, X: pd.DataFrame) -> List[pd.DataFrame]: """Create a list of boolean masks representing the data to mask. + Parameters ---------- X : pd.DataFrame @@ -117,17 +167,19 @@ def split(self, X: pd.DataFrame) -> List[pd.DataFrame]: Returns ------- Dict[str, pd.DataFrame] - the initial dataframe, the dataframe with additional missing entries and the created - mask - """ + the initial dataframe, the dataframe with additional missing + entries and the created mask + """ self.fit(X) list_masks = [] for _ in range(self.n_splits): if self.ngroups is None: mask = self.generate_mask(X) else: - mask = X.groupby(self.ngroups, group_keys=False).apply(self.generate_mask) + mask = X.groupby(self.ngroups, group_keys=False).apply( + self.generate_mask + ) list_masks.append(mask) return list_masks @@ -140,8 +192,10 @@ def _check_subset(self, X: pd.DataFrame): class UniformHoleGenerator(_HoleGenerator): - """This class implements a way to generate holes in a dataframe. - The holes are generated randomly, using the resample method of scikit learn. + """UniformHoleGenerator class. + + This class implements a way to generate holes in a dataframe. + The holes are generated randomly, using the resample method of sklearn. Parameters ---------- @@ -157,6 +211,7 @@ class UniformHoleGenerator(_HoleGenerator): sample_proportional: bool, optional If True, generates holes in target columns with same equal frequency. If False, reproduces the empirical proportions between the variables. + """ def __init__( @@ -177,15 +232,14 @@ def __init__( self.sample_proportional = sample_proportional def generate_mask(self, X: pd.DataFrame) -> pd.DataFrame: - """ - Returns a mask for the dataframe at hand. + """Return a mask for the dataframe at hand. Parameters ---------- X : pd.DataFrame Initial dataframe with a missing pattern to be imitated. - """ + """ self.random_state = sku.check_random_state(self.random_state) df_mask = pd.DataFrame(False, index=X.index, columns=X.columns) @@ -206,8 +260,10 @@ def generate_mask(self, X: pd.DataFrame) -> pd.DataFrame: class _SamplerHoleGenerator(_HoleGenerator): - """This abstract class implements a generic way to generate holes in a dataframe by sampling 1D - hole size distributions. + """Abstract SamplerHoleGenerator class. + + This abstract class implements a generic way to generate holes in a + dataframe by sampling 1D hole size distributions. Parameters ---------- @@ -222,6 +278,7 @@ class _SamplerHoleGenerator(_HoleGenerator): Pass an int for reproducible output across multiple function calls. groups: Tuple[str, ...] Column names used to group the data + """ sample_sizes: Callable @@ -242,18 +299,27 @@ def __init__( groups=groups, ) - def generate_hole_sizes(self, column: str, n_masked: int, sort: bool = True) -> List[int]: - """Generate a sequence of states "states" of size "size" from - a transition matrix "df_transition" + def generate_hole_sizes( + self, column: str, n_masked: int, sort: bool = True + ) -> List[int]: + """Generate a sequence of states "states" of size "size". + + Generated from a transition matrix "df_transition" Parameters ---------- - size : int - length of the output sequence + column : str + column name + n_masked: int + number of masks + sort: bool, optional + true if sort, by default True Returns ------- - List[float] + List[int] + list of hole sizes + """ sizes_sampled = self.sample_sizes(column, n_masked) sizes_sampled = sizes_sampled[sizes_sampled.cumsum() < n_masked] @@ -265,6 +331,7 @@ def generate_hole_sizes(self, column: str, n_masked: int, sort: bool = True) -> def generate_mask(self, X: pd.DataFrame) -> pd.DataFrame: """Create missing data in an arraylike object based on a markov chain. + States of the MC are the different masks of missing values: there are at most pow(2,X.shape[1]) possible states. @@ -277,6 +344,7 @@ def generate_mask(self, X: pd.DataFrame) -> pd.DataFrame: ------- mask : pd.DataFrame masked dataframe with additional missing entries + """ mask = pd.DataFrame(False, columns=X.columns, index=X.index) n_masked_col = round(self.ratio_masked * len(X)) @@ -288,14 +356,29 @@ def generate_mask(self, X: pd.DataFrame) -> pd.DataFrame: sizes_max = get_sizes_max(states) n_masked_left = n_masked_col - sizes_sampled = self.generate_hole_sizes(column, n_masked_col, sort=True) - assert sum(sizes_sampled) == n_masked_col - sizes_sampled += self.generate_hole_sizes(column, n_masked_col, sort=False) + sizes_sampled = self.generate_hole_sizes( + column, n_masked_col, sort=True + ) + if sum(sizes_sampled) != n_masked_col: + raise ValueError( + "sum of sizes_sampled is different from n_masked_col: " + f"{sum(sizes_sampled)} != {n_masked_col}." + ) + sizes_sampled += self.generate_hole_sizes( + column, n_masked_col, sort=False + ) for sample in sizes_sampled: sample = min(min(sample, sizes_max.max()), n_masked_left) i_hole = self.rng.choice(np.where(sample <= sizes_max)[0]) - assert (~mask[column].iloc[i_hole - sample : i_hole]).all() + if not (~mask[column].iloc[i_hole - sample : i_hole]).all(): + raise ValueError( + "The mask condition is not satisfied for " + f"column={column}, " + f"sample={sample}, " + f"and i_hole={i_hole}." + ) + mask[column].iloc[i_hole - sample : i_hole] = True n_masked_left -= sample @@ -308,12 +391,16 @@ def generate_mask(self, X: pd.DataFrame) -> pd.DataFrame: break if list_failed: - warnings.warn(f"No place to introduce sampled holes of size {list_failed}!") + warnings.warn( + f"No place to introduce sampled holes of size {list_failed}!" + ) return mask class GeometricHoleGenerator(_SamplerHoleGenerator): - """This class implements a way to generate holes in a dataframe. + """GeometricHoleGenerator class. + + This class implements a way to generate holes in a dataframe. The holes are generated following a Markov 1D process. Parameters @@ -329,6 +416,7 @@ class GeometricHoleGenerator(_SamplerHoleGenerator): Pass an int for reproducible output across multiple function calls. groups: Tuple[str, ...] Column names used to group the data + """ def __init__( @@ -348,14 +436,13 @@ def __init__( ) def fit(self, X: pd.DataFrame) -> GeometricHoleGenerator: - """ - Get the transition matrix from a list of states + """Get the transition matrix from a list of states. Parameters ---------- X : pd.DataFrame - transition matrix (stochastic matrix) current in index, next in columns - 1 is missing + transition matrix (stochastic matrix) current in index, + next in columns 1 is missing Returns @@ -373,16 +460,35 @@ def fit(self, X: pd.DataFrame) -> GeometricHoleGenerator: return self - def sample_sizes(self, column, n_masked): + def sample_sizes(self, column: str, n_masked: int): + """Sample sizes. + + Parameters + ---------- + column : str + column name + n_masked : int + number of masks + + Returns + ------- + pd.Series + sizes sampled + + """ proba_out = self.dict_probas_out[column] mean_size = 1 / proba_out n_holes = 2 * round(n_masked / mean_size) - sizes_sampled = pd.Series(self.rng.geometric(p=proba_out, size=n_holes)) + sizes_sampled = pd.Series( + self.rng.geometric(p=proba_out, size=n_holes) + ) return sizes_sampled class EmpiricalHoleGenerator(_SamplerHoleGenerator): - """This class implements a way to generate holes in a dataframe. + """EmpiricalHoleGenerator class. + + This class implements a way to generate holes in a dataframe. The distribution of holes is learned from the data. The distributions are learned column by column. @@ -399,6 +505,7 @@ class EmpiricalHoleGenerator(_SamplerHoleGenerator): Pass an int for reproducible output across multiple function calls. groups: Tuple[str, ...] Column names used to group the data + """ def __init__( @@ -418,6 +525,19 @@ def __init__( ) def compute_distribution_holes(self, states: pd.Series) -> pd.Series: + """Compute the hole distribution. + + Parameters + ---------- + states : pd.Series + Series of states. + + Returns + ------- + pd.Series + hole distribution + + """ series_id = (states.diff() != 0).cumsum() series_id = series_id[states] distribution_holes = series_id.value_counts().value_counts() @@ -427,7 +547,8 @@ def compute_distribution_holes(self, states: pd.Series) -> pd.Series: def fit(self, X: pd.DataFrame) -> EmpiricalHoleGenerator: """Compute the holes sizes of a dataframe. - Dataframe df has only one column + + Dataframe df has only one column. Parameters ---------- @@ -438,6 +559,7 @@ def fit(self, X: pd.DataFrame) -> EmpiricalHoleGenerator: ------- EmpiricalTimeHoleGenerator The model itself + """ super().fit(X) @@ -445,42 +567,54 @@ def fit(self, X: pd.DataFrame) -> EmpiricalHoleGenerator: for column in self.subset: states = X[column].isna() if self.ngroups is None: - self.dict_distributions_holes[column] = self.compute_distribution_holes(states) + self.dict_distributions_holes[column] = ( + self.compute_distribution_holes(states) + ) else: distributions_holes = states.groupby(self.ngroups).apply( self.compute_distribution_holes ) - distributions_holes = distributions_holes.groupby(by="_size_hole").sum() + distributions_holes = distributions_holes.groupby( + by="_size_hole" + ).sum() self.dict_distributions_holes[column] = distributions_holes return self def sample_sizes(self, column, n_masked): - """Create missing data in an arraylike object based on the holes size distribution. + """Create missing data based on the holes size distribution. Parameters ---------- column : str name of the column to fill with holes - nb_holes : Optional[int], optional - number of holes to create, by default 10 + n_masked :int + number of masks Returns ------- samples_sizes : List[int] + """ distribution_holes = self.dict_distributions_holes[column] distribution_holes /= distribution_holes.sum() - mean_size = (distribution_holes.values * distribution_holes.index.values).sum() + mean_size = ( + distribution_holes.values * distribution_holes.index.values + ).sum() n_samples = 2 * round(n_masked / mean_size) - sizes_sampled = self.rng.choice(distribution_holes.index, n_samples, p=distribution_holes) + sizes_sampled = self.rng.choice( + distribution_holes.index, n_samples, p=distribution_holes + ) return sizes_sampled class MultiMarkovHoleGenerator(_HoleGenerator): - """This class implements a way to generate holes in a dataframe. + """MultiMarkovHoleGenerator class. + + This class implements a way to generate holes in a dataframe. The holes are generated according to a Markov process. - Each line of the dataframe mask (np.nan) represents a state of the Markov chain. + Each line of the dataframe mask (np.nan) represents a state of the + Markov chain. Parameters ---------- @@ -495,6 +629,7 @@ class MultiMarkovHoleGenerator(_HoleGenerator): Pass an int for reproducible output across multiple function calls. groups: Tuple[str, ...] Column names used to group the data + """ def __init__( @@ -514,7 +649,8 @@ def __init__( ) def fit(self, X: pd.DataFrame) -> MultiMarkovHoleGenerator: - """ + """Get the transition matrix. + Get the transition matrix from a list of states transition matrix (stochastic matrix) current in index, next in columns 1 is missing @@ -522,6 +658,7 @@ def fit(self, X: pd.DataFrame) -> MultiMarkovHoleGenerator: Parameters ---------- X : pd.DataFrame + input dataframe Returns ------- @@ -533,28 +670,34 @@ def fit(self, X: pd.DataFrame) -> MultiMarkovHoleGenerator: states = X[self.subset].isna().apply(lambda x: tuple(x), axis=1) self.df_transition = compute_transition_matrix(states, self.ngroups) - self.df_transition.index = pd.MultiIndex.from_tuples(self.df_transition.index) - self.df_transition.columns = pd.MultiIndex.from_tuples(self.df_transition.columns) + self.df_transition.index = pd.MultiIndex.from_tuples( + self.df_transition.index + ) + self.df_transition.columns = pd.MultiIndex.from_tuples( + self.df_transition.columns + ) return self - def generate_multi_realisation(self, n_masked: int) -> List[List[Tuple[bool, ...]]]: - """Generate a sequence of states "states" of size "size" - from a transition matrix "df_transition" + def generate_multi_realisation( + self, n_masked: int + ) -> List[List[Tuple[bool, ...]]]: + """Generate a sequence of states "states" of size "size". + + Generated from a transition matrix "df_transition" Parameters ---------- - df_transition : pd.DataFrame - transition matrix (stochastic matrix) - size : int - length of the output sequence + n_masked : int + number of masks. Returns ------- realisation ; List[int] sequence of states + """ - states = sorted(list(self.df_transition.index)) + states = sorted(self.df_transition.index) state_nona = tuple([False] * len(states[0])) state = state_nona @@ -564,7 +707,9 @@ def generate_multi_realisation(self, n_masked: int) -> List[List[Tuple[bool, ... realisation = [] while True: probas = self.df_transition.loc[state, :].values - state = np.random.choice(self.df_transition.columns, 1, p=probas)[0] + state = np.random.choice( + self.df_transition.columns, 1, p=probas + )[0] if state == state_nona: break else: @@ -576,6 +721,7 @@ def generate_multi_realisation(self, n_masked: int) -> List[List[Tuple[bool, ... def generate_mask(self, X: pd.DataFrame) -> List[pd.DataFrame]: """Create missing data in an arraylike object based on a markov chain. + States of the MC are the different masks of missing values: there are at most pow(2,X.shape[1]) possible states. @@ -587,13 +733,15 @@ def generate_mask(self, X: pd.DataFrame) -> List[pd.DataFrame]: Returns ------- Dict[str, pd.DataFrame] - the initial dataframe, the dataframe with additional missing entries and the created - mask - """ + the initial dataframe, the dataframe with additional missing + entries and the created mask + """ self.rng = sku.check_random_state(self.random_state) X_subset = X[self.subset] - mask = pd.DataFrame(False, columns=X_subset.columns, index=X_subset.index) + mask = pd.DataFrame( + False, columns=X_subset.columns, index=X_subset.index + ) values_hasna = X_subset.isna().any(axis=1) @@ -608,7 +756,11 @@ def generate_mask(self, X: pd.DataFrame) -> List[pd.DataFrame]: size_hole = min(size_hole, sizes_max.max()) realisation = realisation[:size_hole] i_hole = self.rng.choice(np.where(size_hole <= sizes_max)[0]) - assert (~mask.iloc[i_hole - size_hole : i_hole]).all().all() + if not (~mask.iloc[i_hole - size_hole : i_hole]).all().all(): + raise ValueError( + f"The mask condition is not satisfied for i_hole={i_hole} " + f"and size_hole={size_hole}." + ) if size_hole != 0: mask.iloc[i_hole - size_hole : i_hole] = mask.iloc[ i_hole - size_hole : i_hole @@ -629,7 +781,9 @@ def generate_mask(self, X: pd.DataFrame) -> List[pd.DataFrame]: class GroupedHoleGenerator(_HoleGenerator): - """This class implements a way to generate holes in a dataframe. + """GroupedHoleGenerator class. + + This class implements a way to generate holes in a dataframe. The holes are generated from groups, specified by the user. Parameters @@ -645,6 +799,7 @@ class GroupedHoleGenerator(_HoleGenerator): Pass an int for reproducible output across multiple function calls. groups : Tuple[str, ...] Names of the columns forming the groups, by default [] + """ def __init__( @@ -667,11 +822,12 @@ def __init__( raise Exception("Argument groups is an empty tuple!") def fit(self, X: pd.DataFrame) -> GroupedHoleGenerator: - """Create the groups based on the column names (groups attribute) + """Create the groups based on the column names (groups attribute). Parameters ---------- X : pd.DataFrame + input dataframe Returns ------- @@ -681,33 +837,41 @@ def fit(self, X: pd.DataFrame) -> GroupedHoleGenerator: Raises ------ if the number of samples/splits is greater than the number of groups. - """ + """ super().fit(X) if self.n_splits > self.ngroups.nunique(): - raise ValueError("n_samples has to be smaller than the number of groups.") + raise ValueError( + "n_samples has to be smaller than the number of groups." + ) return self def split(self, X: pd.DataFrame) -> List[pd.DataFrame]: - """creates masked dataframes + """Create masked dataframes. Parameters ---------- X : pd.DataFrame + input dataframe Returns ------- List[pd.DataFrame] list of masks + """ self.fit(X) - group_sizes = X.groupby(self.ngroups, group_keys=False).count().mean(axis=1) + group_sizes = ( + X.groupby(self.ngroups, group_keys=False).count().mean(axis=1) + ) list_masks = [] for _ in range(self.n_splits): - shuffled_group_sizes = group_sizes.sample(frac=1, random_state=self.random_state) + shuffled_group_sizes = group_sizes.sample( + frac=1, random_state=self.random_state + ) ratio_masks = shuffled_group_sizes.cumsum() / len(X) ratio_masks = ratio_masks.reset_index(name="ratio") @@ -715,7 +879,9 @@ def split(self, X: pd.DataFrame) -> List[pd.DataFrame]: closest_ratio_mask = ratio_masks.iloc[ (ratio_masks["ratio"] - self.ratio_masked).abs().argsort()[:1] ] - groups_masked = ratio_masks.iloc[: closest_ratio_mask.index[0], :]["_ngroup"].values + groups_masked = ratio_masks.iloc[: closest_ratio_mask.index[0], :][ + "_ngroup" + ].values if closest_ratio_mask.index[0] == 0: groups_masked = ratio_masks.iloc[:1, :]["_ngroup"].values diff --git a/qolmat/imputations/diffusions/base.py b/qolmat/imputations/diffusions/base.py index 84fe339d..1b6a9abd 100644 --- a/qolmat/imputations/diffusions/base.py +++ b/qolmat/imputations/diffusions/base.py @@ -1,19 +1,24 @@ +"""Script for base classes.""" + +import math from typing import Tuple + import torch -import math class ResidualBlock(torch.nn.Module): - """Residual block based on the work of Gorishniy et al., 2023 + """ResidualBlock. + + Based on the work of Gorishniy et al., 2023 (https://arxiv.org/abs/2106.11959). We follow the implementation found in - https://github.com/Yura52/rtdl/blob/main/rtdl/nn/_backbones.py""" + https://github.com/Yura52/rtdl/blob/main/rtdl/nn/_backbones.py + """ - def __init__(self, dim_input: int, dim_embedding: int = 128, p_dropout: float = 0.0): - """Residual block based on the work of Gorishniy et al., 2023 - (https://arxiv.org/abs/2106.11959). - We follow the implementation found in - https://github.com/Yura52/rtdl/blob/main/rtdl/nn/_backbones.py + def __init__( + self, dim_input: int, dim_embedding: int = 128, p_dropout: float = 0.0 + ): + """Init funciton. Parameters ---------- @@ -23,8 +28,8 @@ def __init__(self, dim_input: int, dim_embedding: int = 128, p_dropout: float = Embedding dimension, by default 128 p_dropout : float, optional Dropout probability, by default 0.1 - """ + """ super().__init__() self.layer_norm = torch.nn.LayerNorm(dim_input) @@ -34,8 +39,10 @@ def __init__(self, dim_input: int, dim_embedding: int = 128, p_dropout: float = self.linear_out = torch.nn.Linear(dim_embedding, dim_input) - def forward(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: - """Return an output of a residual block + def forward( + self, x: torch.Tensor, t: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Return an output of a residual block. Parameters ---------- @@ -48,8 +55,8 @@ def forward(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, torch ------- Tuple[torch.Tensor, torch.Tensor] Output data at noise step t - """ + """ x_t = self.layer_norm(x + t) x_t_emb = torch.nn.functional.relu(self.linear_in(x_t)) x_t_emb = self.dropout(x_t_emb) @@ -59,12 +66,15 @@ def forward(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, torch class ResidualBlockTS(torch.nn.Module): - """Residual block based on the work of Gorishniy et al., 2023 + """Residual block time series. + + Residual block based on the work of Gorishniy et al., 2023 (https://arxiv.org/abs/2106.11959). We follow the implementation found in https://github.com/Yura52/rtdl/blob/main/rtdl/nn/_backbones.py This class is for Time-Series data where we add Tranformers to - encode time-based/feature-based context.""" + encode time-based/feature-based context. + """ def __init__( self, @@ -76,12 +86,7 @@ def __init__( nheads_time: int = 8, num_layers_transformer: int = 1, ): - """Residual block based on the work of Gorishniy et al., 2023 - (https://arxiv.org/abs/2106.11959). - We follow the implementation found in - https://github.com/Yura52/rtdl/blob/main/rtdl/nn/_backbones.py - This class is for Time-Series data where we add Tranformers to - encode time-based/feature-based context. + """Init function. Parameters ---------- @@ -99,6 +104,7 @@ def __init__( Number of heads to encode time-based context, by default 8 num_layers_transformer : int, optional Number of transformer layer, by default 1 + """ super().__init__() @@ -118,8 +124,10 @@ def __init__( self.linear_out = torch.nn.Linear(dim_embedding, dim_input) - def forward(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: - """Return an output of a residual block + def forward( + self, x: torch.Tensor, t: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Return an output of a residual block. Parameters ---------- @@ -132,12 +140,15 @@ def forward(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, torch ------- torch.Tensor Data output, noise predicted + """ batch_size, size_window, dim_emb = x.shape x_emb = self.layer_norm(x) x_emb_time = self.time_layer(x_emb) - t_emb = t.repeat(1, size_window).reshape(batch_size, size_window, dim_emb) + t_emb = t.repeat(1, size_window).reshape( + batch_size, size_window, dim_emb + ) x_t = x + x_emb_time + t_emb x_t = self.linear_out(x_t) @@ -146,11 +157,14 @@ def forward(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, torch class AutoEncoder(torch.nn.Module): - """Epsilon_theta model of the Algorithm 1 in + """Auto encoder class. + + Epsilon_theta model of the Algorithm 1 in Ho et al., 2020 (https://arxiv.org/abs/2006.11239). This implementation is based on the work of Tashiro et al., 2021 (https://arxiv.org/abs/2107.03502). - Their code: https://github.com/ermongroup/CSDI/blob/main/diff_models.py""" + Their code: https://github.com/ermongroup/CSDI/blob/main/diff_models.py + """ def __init__( self, @@ -161,8 +175,7 @@ def __init__( num_blocks: int = 1, p_dropout: float = 0.0, ): - """Epsilon_theta model in Algorithm 1 in - Ho et al., 2020 (https://arxiv.org/abs/2006.11239) + """Init function. Parameters ---------- @@ -170,12 +183,15 @@ def __init__( Number of steps in forward/reverse processes dim_input : int Input dimension + residual_block: torch.nn.Module + residual blocks dim_embedding : int, optional Embedding dimension, by default 128 num_blocks : int, optional Number of residual blocks, by default 1 p_dropout : float, optional Dropout probability, by default 0.0 + """ super().__init__() @@ -193,10 +209,12 @@ def __init__( self.layer_out_2 = torch.nn.Linear(dim_embedding, dim_input) self.dropout_out = torch.nn.Dropout(p_dropout) - self.residual_layers = torch.nn.ModuleList([residual_block for _ in range(num_blocks)]) + self.residual_layers = torch.nn.ModuleList( + [residual_block for _ in range(num_blocks)] + ) def forward(self, x: torch.Tensor, t: torch.LongTensor) -> torch.Tensor: - """Predict a noise + """Predict a noise. Parameters ---------- @@ -209,6 +227,7 @@ def forward(self, x: torch.Tensor, t: torch.LongTensor) -> torch.Tensor: ------- torch.Tensor Data output, noise predicted + """ # Noise step embedding t_emb = torch.as_tensor(self.embedding_noise_step)[t].squeeze() @@ -224,15 +243,20 @@ def forward(self, x: torch.Tensor, t: torch.LongTensor) -> torch.Tensor: x_emb, skip_connection = layer(x_emb, t_emb) skip.append(skip_connection) - out = torch.sum(torch.stack(skip), dim=0) / math.sqrt(len(self.residual_layers)) + out = torch.sum(torch.stack(skip), dim=0) / math.sqrt( + len(self.residual_layers) + ) out = torch.nn.functional.relu(self.layer_out_1(out)) out = self.dropout_out(out) out = self.layer_out_2(out) return out - def _build_embedding(self, num_noise_steps: int, dim: int = 64) -> torch.Tensor: + def _build_embedding( + self, num_noise_steps: int, dim: int = 64 + ) -> torch.Tensor: """Build an embedding for noise step. + More details in section E.1 of Tashiro et al., 2021 (https://arxiv.org/abs/2107.03502) @@ -247,9 +271,14 @@ def _build_embedding(self, num_noise_steps: int, dim: int = 64) -> torch.Tensor: ------- torch.Tensor List of embeddings for noise steps + """ steps = torch.arange(num_noise_steps).unsqueeze(1) # (T,1) - frequencies = 10.0 ** (torch.arange(dim) / (dim - 1) * 4.0).unsqueeze(0) # (1,dim) + frequencies = 10.0 ** (torch.arange(dim) / (dim - 1) * 4.0).unsqueeze( + 0 + ) # (1,dim) table = steps * frequencies # (T,dim) - table = torch.cat([torch.sin(table), torch.cos(table)], dim=1) # (T,dim*2) + table = torch.cat( + [torch.sin(table), torch.cos(table)], dim=1 + ) # (T,dim*2) return table diff --git a/qolmat/imputations/diffusions/ddpms.py b/qolmat/imputations/diffusions/ddpms.py index 231f870e..4f8728e9 100644 --- a/qolmat/imputations/diffusions/ddpms.py +++ b/qolmat/imputations/diffusions/ddpms.py @@ -1,25 +1,31 @@ -from typing import Dict, List, Callable, Tuple, Union -from typing_extensions import Self -import sys -import numpy as np -import pandas as pd +"""Script for DDPM classes.""" + import time from datetime import timedelta -from tqdm import tqdm +from typing import Callable, Dict, List, Tuple, Union +import numpy as np +import pandas as pd import torch -from torch.utils.data import DataLoader, TensorDataset from sklearn import preprocessing from sklearn import utils as sku +from torch.utils.data import DataLoader, TensorDataset +from tqdm import tqdm - -from qolmat.imputations.diffusions.base import AutoEncoder, ResidualBlock, ResidualBlockTS +# from typing_extensions import Self +from qolmat.benchmark import metrics, missing_patterns +from qolmat.imputations.diffusions.base import ( + AutoEncoder, + ResidualBlock, + ResidualBlockTS, +) from qolmat.imputations.diffusions.utils import get_num_params -from qolmat.benchmark import missing_patterns, metrics class TabDDPM: - """Diffusion model for tabular data based on + """Tab DDPM. + + Diffusion model for tabular data based on Denoising Diffusion Probabilistic Models (DDPM) of Ho et al., 2020 (https://arxiv.org/abs/2006.11239), Tashiro et al., 2021 (https://arxiv.org/abs/2107.03502). @@ -42,13 +48,7 @@ def __init__( is_clip: bool = True, random_state: Union[None, int, np.random.RandomState] = None, ): - """Diffusion model for tabular data based on - Denoising Diffusion Probabilistic Models (DDPM) of - Ho et al., 2020 (https://arxiv.org/abs/2006.11239), - Tashiro et al., 2021 (https://arxiv.org/abs/2107.03502). - This implementation follows the implementations found in - https://github.com/quickgrid/pytorch-diffusion/tree/main, - https://github.com/ermongroup/CSDI/tree/main + """Init function. Parameters ---------- @@ -70,11 +70,18 @@ def __init__( Dropout probability, by default 0.0 num_sampling : int, optional Number of samples generated for each cell, by default 1 + is_clip : bool, optional + if values have to be clipped, by default True random_state : int, RandomState instance or None, default=None Controls the randomness. Pass an int for reproducible output across multiple function calls. + """ - self.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") + self.device = ( + torch.device("cuda") + if torch.cuda.is_available() + else torch.device("cpu") + ) # Hyper-parameters for DDPM # Section 2, equation 1, num_noise_steps is T. @@ -92,7 +99,8 @@ def __init__( self.alpha = 1 - self.beta self.alpha_hat = torch.cumprod(self.alpha, dim=0) - # Section 3.2, algorithm 1 formula implementation. Generate values early reuse later. + # Section 3.2, algorithm 1 formula implementation. + # Generate values early reuse later. self.sqrt_alpha_hat = torch.sqrt(self.alpha_hat) self.sqrt_one_minus_alpha_hat = torch.sqrt(1 - self.alpha_hat) @@ -117,10 +125,14 @@ def __init__( seed_torch = self.random_state.randint(2**31 - 1) torch.manual_seed(seed_torch) - def _q_sample(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: - """Section 3.2, algorithm 1 formula implementation. Forward process, defined by `q`. - Found in section 2. `q` gradually adds gaussian noise according to variance schedule. Also, - can be seen on figure 2. + def _q_sample( + self, x: torch.Tensor, t: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Sample q. + + Section 3.2, algorithm 1 formula implementation. Forward process, + defined by `q`. Found in section 2. `q` gradually adds gaussian noise + according to variance schedule. Also, can be seen on figure 2. Ho et al., 2020 (https://arxiv.org/abs/2006.11239) Parameters @@ -134,8 +146,8 @@ def _q_sample(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, tor ------- Tuple[torch.Tensor, torch.Tensor] Noised data at noise step t - """ + """ sqrt_alpha_hat = self.sqrt_alpha_hat[t].view(-1, 1) sqrt_one_minus_alpha_hat = self.sqrt_one_minus_alpha_hat[t].view(-1, 1) @@ -146,16 +158,20 @@ def _set_eps_model(self) -> None: self._eps_model = AutoEncoder( num_noise_steps=self.num_noise_steps, dim_input=self.dim_input, - residual_block=ResidualBlock(self.dim_embedding, self.dim_embedding, self.p_dropout), + residual_block=ResidualBlock( + self.dim_embedding, self.dim_embedding, self.p_dropout + ), dim_embedding=self.dim_embedding, num_blocks=self.num_blocks, p_dropout=self.p_dropout, ).to(self.device) - self.optimiser = torch.optim.Adam(self._eps_model.parameters(), lr=self.lr) + self.optimiser = torch.optim.Adam( + self._eps_model.parameters(), lr=self.lr + ) def _print_valid(self, epoch: int, time_duration: float) -> None: - """Print model performance on validation data + """Print model performance on validation data. Parameters ---------- @@ -163,22 +179,31 @@ def _print_valid(self, epoch: int, time_duration: float) -> None: Epoch of the printed performance time_duration : float Duration for training step + """ self.time_durations.append(time_duration) print_step = 1 if int(self.epochs / 10) == 0 else int(self.epochs / 10) if self.print_valid and epoch == 0: - print(f"Num params of {self.__class__.__name__}: {self.num_params}") + print( + f"Num params of {self.__class__.__name__}: {self.num_params}" + ) if self.print_valid and epoch % print_step == 0: string_valid = f"Epoch {epoch}: " for s in self.summary: - string_valid += f" {s}={round(self.summary[s][epoch], self.round)}" + string_valid += ( + f" {s}={round(self.summary[s][epoch], self.round)}" + ) # string_valid += f" | in {round(time_duration, 3)} secs" - remaining_duration = np.mean(self.time_durations) * (self.epochs - epoch) - string_valid += f" | remaining {timedelta(seconds=remaining_duration)}" + remaining_duration = np.mean(self.time_durations) * ( + self.epochs - epoch + ) + string_valid += ( + f" | remaining {timedelta(seconds=remaining_duration)}" + ) print(string_valid) def _impute(self, x: np.ndarray, x_mask_obs: np.ndarray) -> np.ndarray: - """Impute data array + """Impute data array. Parameters ---------- @@ -191,6 +216,7 @@ def _impute(self, x: np.ndarray, x_mask_obs: np.ndarray) -> np.ndarray: ------- np.ndarray Imputed data + """ x_tensor = torch.from_numpy(x).float().to(self.device) x_mask_tensor = torch.from_numpy(x_mask_obs).float().to(self.device) @@ -207,37 +233,55 @@ def _impute(self, x: np.ndarray, x_mask_obs: np.ndarray) -> np.ndarray: for i in reversed(range(1, self.num_noise_steps)): t = ( - torch.ones((x_batch.size(dim=0), 1), dtype=torch.long, device=self.device) + torch.ones( + (x_batch.size(dim=0), 1), + dtype=torch.long, + device=self.device, + ) * i ) if len(x_batch.size()) == 3: - # Data are splited into chunks (i.e., Time-series data), a window of rows + # Data are splited into chunks + # (i.e., Time-series data), + # a window of rows # is processed. sqrt_alpha_t = self.sqrt_alpha[t].view(-1, 1, 1) beta_t = self.beta[t].view(-1, 1, 1) - sqrt_one_minus_alpha_hat_t = self.sqrt_one_minus_alpha_hat[t].view( - -1, 1, 1 + sqrt_one_minus_alpha_hat_t = ( + self.sqrt_one_minus_alpha_hat[t].view(-1, 1, 1) ) epsilon_t = self.std_beta[t].view(-1, 1, 1) else: # Each row of data is separately processed. sqrt_alpha_t = self.sqrt_alpha[t].view(-1, 1) beta_t = self.beta[t].view(-1, 1) - sqrt_one_minus_alpha_hat_t = self.sqrt_one_minus_alpha_hat[t].view(-1, 1) + sqrt_one_minus_alpha_hat_t = ( + self.sqrt_one_minus_alpha_hat[t].view(-1, 1) + ) epsilon_t = self.std_beta[t].view(-1, 1) - random_noise = torch.randn_like(noise) if i > 1 else torch.zeros_like(noise) + random_noise = ( + torch.randn_like(noise) + if i > 1 + else torch.zeros_like(noise) + ) noise = ( (1 / sqrt_alpha_t) * ( noise - - ((beta_t / sqrt_one_minus_alpha_hat_t) * self._eps_model(noise, t)) + - ( + (beta_t / sqrt_one_minus_alpha_hat_t) + * self._eps_model(noise, t) + ) ) ) + (epsilon_t * random_noise) - noise = mask_x_batch * x_batch + (1.0 - mask_x_batch) * noise + noise = ( + mask_x_batch * x_batch + (1.0 - mask_x_batch) * noise + ) - # Generate data output, this activation function depends on normalizer_x + # Generate data output, this activation function depends on + # normalizer_x x_out = noise.detach().cpu().numpy() outputs.append(x_out) @@ -252,7 +296,7 @@ def _eval( x_mask_obs_df: pd.DataFrame, x_indices: List, ) -> Dict: - """Evaluate the model + """Evaluate the model. Parameters ---------- @@ -271,8 +315,8 @@ def _eval( ------- Dict Scores - """ + """ list_x_imputed = [] for i in tqdm(range(self.num_sampling), disable=True, leave=False): x_imputed = self._impute(x, x_mask_obs) @@ -289,7 +333,9 @@ def _eval( x_final.loc[x_out.index] = x_out.loc[x_out.index] x_mask_imputed_df = ~x_mask_obs_df - columns_with_True = x_mask_imputed_df.columns[(x_mask_imputed_df == True).any()] + columns_with_True = x_mask_imputed_df.columns[ + (x_mask_imputed_df).any() + ] scores = {} for metric in self.metrics_valid: scores[metric.__name__] = metric( @@ -300,9 +346,12 @@ def _eval( return scores def _process_data( - self, x: pd.DataFrame, mask: pd.DataFrame = None, is_training: bool = False + self, + x: pd.DataFrame, + mask: pd.DataFrame = None, + is_training: bool = False, ) -> Tuple[np.ndarray, np.ndarray, List]: - """Pre-process data + """Pre-process data. Parameters ---------- @@ -317,10 +366,13 @@ def _process_data( ------- Tuple[np.ndarray, np.ndarray] Data and mask pre-processed + """ if is_training: self.normalizer_x.fit(x.values) - x_windows_processed = self.normalizer_x.transform(x.fillna(x.mean()).values) + x_windows_processed = self.normalizer_x.transform( + x.fillna(x.mean()).values + ) x_windows_mask_processed = ~x.isna().to_numpy() if mask is not None: x_windows_mask_processed = mask.to_numpy() @@ -332,7 +384,9 @@ def _process_reversely_data( ): x_normalized = self.normalizer_x.inverse_transform(x_imputed) x_normalized = x_normalized[: x_input.shape[0]] - x_out = pd.DataFrame(x_normalized, columns=self.columns, index=x_input.index) + x_out = pd.DataFrame( + x_normalized, columns=self.columns, index=x_input.index + ) x_final = x_input.copy() x_final.loc[x_out.index] = x_out.loc[x_out.index] @@ -352,8 +406,8 @@ def fit( ), round: int = 10, cols_imputed: Tuple[str, ...] = (), - ) -> Self: - """Fit data + ) -> "TabDDPM": + """Fit data. Parameters ---------- @@ -368,8 +422,8 @@ def fit( x_valid : pd.DataFrame, optional Dataframe for validation, by default None metrics_valid : Tuple[Callable, ...], optional - Set of validation metrics, by default ( metrics.mean_absolute_error, - metrics.dist_wasserstein ) + Set of validation metrics, by default (metrics.mean_absolute_error, + metrics.dist_wasserstein) round : int, optional Number of decimal places to round to, for better displaying model performance, by default 10 @@ -380,10 +434,12 @@ def fit( ------ ValueError Batch size is larger than data size + Returns ------- Self Return Self + """ self.dim_input = len(x.columns) self.epochs = epochs @@ -398,23 +454,29 @@ def fit( if len(self.cols_imputed) != 0: self.cols_idx_not_imputed = [ - idx for idx, col in enumerate(self.columns) if col not in self.cols_imputed + idx + for idx, col in enumerate(self.columns) + if col not in self.cols_imputed ] - self.interval_x = {col: [x[col].min(), x[col].max()] for col in self.columns} + self.interval_x = { + col: [x[col].min(), x[col].max()] for col in self.columns + } # x_mask: 1 for observed values, 0 for nan x_processed, x_mask, _ = self._process_data(x, is_training=True) if self.batch_size > x_processed.shape[0]: raise ValueError( - f"Batch size {self.batch_size} larger than size of pre-processed x" - + f" size={x_processed.shape[0]}. Please reduce batch_size." - + " In the case of TabDDPMTS, you can also reduce freq_str." + f"Batch size {self.batch_size} larger than size of " + "pre-processed x " + f"size={x_processed.shape[0]}. Please reduce batch_size. " + "In the case of TabDDPMTS, you can also reduce freq_str." ) if x_valid is not None: - # We reuse the UniformHoleGenerator to generate artificial holes (with one mask) + # We reuse the UniformHoleGenerator to generate artificial holes + # (with one mask) # in validation dataset x_valid_mask = missing_patterns.UniformHoleGenerator( n_splits=1, ratio_masked=self.ratio_nan @@ -425,7 +487,9 @@ def fit( x_processed_valid, x_processed_valid_obs_mask, x_processed_valid_indices, - ) = self._process_data(x_valid, x_valid_obs_mask, is_training=False) + ) = self._process_data( + x_valid, x_valid_obs_mask, is_training=False + ) x_tensor = torch.from_numpy(x_processed).float().to(self.device) x_mask_tensor = torch.from_numpy(x_mask).float().to(self.device) @@ -447,7 +511,10 @@ def fit( time_start = time.time() self._eps_model.train() for id_batch, (x_batch, mask_x_batch) in enumerate(dataloader): - mask_obs_rand = torch.FloatTensor(mask_x_batch.size()).uniform_() > self.ratio_nan + mask_obs_rand = ( + torch.FloatTensor(mask_x_batch.size()).uniform_() + > self.ratio_nan + ) for col in self.cols_idx_not_imputed: mask_obs_rand[:, col] = 0.0 mask_x_batch = mask_x_batch * mask_obs_rand.to(self.device) @@ -461,7 +528,9 @@ def fit( ) x_batch_t, noise = self._q_sample(x=x_batch, t=t) predicted_noise = self._eps_model(x=x_batch_t, t=t) - loss = (self.loss_func(predicted_noise, noise) * mask_x_batch).mean() + loss = ( + self.loss_func(predicted_noise, noise) * mask_x_batch + ).mean() loss.backward() self.optimiser.step() loss_epoch += loss.item() @@ -487,7 +556,7 @@ def fit( return self def predict(self, x: pd.DataFrame) -> pd.DataFrame: - """Predict/impute data + """Predict/impute data. Parameters ---------- @@ -498,10 +567,13 @@ def predict(self, x: pd.DataFrame) -> pd.DataFrame: ------- pd.DataFrame Imputed data + """ self._eps_model.eval() - x_processed, x_mask, x_indices = self._process_data(x, is_training=False) + x_processed, x_mask, x_indices = self._process_data( + x, is_training=False + ) list_x_imputed = [] for i in tqdm(range(self.num_sampling), leave=False): @@ -519,7 +591,9 @@ def predict(self, x: pd.DataFrame) -> pd.DataFrame: class TsDDPM(TabDDPM): - """Diffusion model for time-series data based on + """Time series DDPM. + + Diffusion model for time-series data based on Denoising Diffusion Probabilistic Models (DDPMs) of Ho et al., 2020 (https://arxiv.org/abs/2006.11239), Tashiro et al., 2021 (https://arxiv.org/abs/2107.03502). @@ -546,12 +620,7 @@ def __init__( is_rolling: bool = False, random_state: Union[None, int, np.random.RandomState] = None, ): - """Diffusion model for time-series data based on the works of - Ho et al., 2020 (https://arxiv.org/abs/2006.11239), - Tashiro et al., 2021 (https://arxiv.org/abs/2107.03502). - This implementation follows the implementations found in - https://github.com/quickgrid/pytorch-diffusion/tree/main, - https://github.com/ermongroup/CSDI/tree/main + """Init function. Parameters ---------- @@ -582,10 +651,12 @@ def __init__( num_sampling : int, optional Number of samples generated for each cell, by default 1 is_rolling : bool, optional - Use pandas.DataFrame.rolling for preprocessing data, by default False + Use pandas.DataFrame.rolling for preprocessing data, + by default False random_state : int, RandomState instance or None, default=None Controls the randomness. Pass an int for reproducible output across multiple function calls. + """ super().__init__( num_noise_steps, @@ -606,10 +677,14 @@ def __init__( self.num_layers_transformer = num_layers_transformer self.is_rolling = is_rolling - def _q_sample(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: - """Section 3.2, algorithm 1 formula implementation. Forward process, defined by `q`. - Found in section 2. `q` gradually adds gaussian noise according to variance schedule. Also, - can be seen on figure 2. + def _q_sample( + self, x: torch.Tensor, t: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Sample q. + + Section 3.2, algorithm 1 formula implementation. Forward process, + defined by `q`. Found in section 2. `q` gradually adds gaussian noise + according to variance schedule. Also, can be seen on figure 2. Parameters ---------- @@ -622,10 +697,12 @@ def _q_sample(self, x: torch.Tensor, t: torch.Tensor) -> Tuple[torch.Tensor, tor ------- Tuple[torch.Tensor, torch.Tensor] Noised data at noise step t - """ + """ sqrt_alpha_hat = self.sqrt_alpha_hat[t].view(-1, 1, 1) - sqrt_one_minus_alpha_hat = self.sqrt_one_minus_alpha_hat[t].view(-1, 1, 1) + sqrt_one_minus_alpha_hat = self.sqrt_one_minus_alpha_hat[t].view( + -1, 1, 1 + ) epsilon = torch.randn_like(x, device=self.device) return sqrt_alpha_hat * x + sqrt_one_minus_alpha_hat * epsilon, epsilon @@ -648,12 +725,17 @@ def _set_eps_model(self): p_dropout=self.p_dropout, ).to(self.device) - self.optimiser = torch.optim.Adam(self._eps_model.parameters(), lr=self.lr) + self.optimiser = torch.optim.Adam( + self._eps_model.parameters(), lr=self.lr + ) def _process_data( - self, x: pd.DataFrame, mask: pd.DataFrame = None, is_training: bool = False + self, + x: pd.DataFrame, + mask: pd.DataFrame = None, + is_training: bool = False, ) -> Tuple[np.ndarray, np.ndarray, List]: - """Pre-process data + """Pre-process data. Parameters ---------- @@ -668,30 +750,45 @@ def _process_data( ------- Tuple[np.ndarray, np.ndarray] Data and mask pre-processed + """ if is_training: self.normalizer_x.fit(x.values) x_windows: List = [] x_windows_indices: List = [] - columns_index = [col for col in x.index.names if col != self.index_datetime] + columns_index = [ + col for col in x.index.names if col != self.index_datetime + ] if is_training: if self.is_rolling: if self.print_valid: print( - "Preprocessing data with sliding window (pandas.DataFrame.rolling)" - + " can require more times than usual. Please be patient!" + "Preprocessing data with sliding window " + "(pandas.DataFrame.rolling) " + "can require more times than usual. " + "Please be patient!" ) if len(columns_index) == 0: x_windows = x.rolling(window=self.freq_str) else: - columns_index_ = columns_index[0] if len(columns_index) == 1 else columns_index - for x_group in tqdm(x.groupby(by=columns_index_), disable=True, leave=False): + columns_index_ = ( + columns_index[0] + if len(columns_index) == 1 + else columns_index + ) + for x_group in tqdm( + x.groupby(by=columns_index_), disable=True, leave=False + ): x_windows += list( - x_group[1].droplevel(columns_index).rolling(window=self.freq_str) + x_group[1] + .droplevel(columns_index) + .rolling(window=self.freq_str) ) else: - for x_w in x.resample(rule=self.freq_str, level=self.index_datetime): + for x_w in x.resample( + rule=self.freq_str, level=self.index_datetime + ): x_windows.append(x_w[1]) else: if self.is_rolling: @@ -703,23 +800,43 @@ def _process_data( x_windows.append(x_rolling) x_windows_indices.append(x_rolling.index) else: - columns_index_ = columns_index[0] if len(columns_index) == 1 else columns_index - for x_group in tqdm(x.groupby(by=columns_index_), disable=True, leave=False): - x_group_index = [x_group[0]] if len(columns_index) == 1 else x_group[0] + columns_index_ = ( + columns_index[0] + if len(columns_index) == 1 + else columns_index + ) + for x_group in tqdm( + x.groupby(by=columns_index_), disable=True, leave=False + ): + x_group_index = ( + [x_group[0]] + if len(columns_index) == 1 + else x_group[0] + ) x_group_value = x_group[1].droplevel(columns_index) - indices_nan = x_group_value.loc[x_group_value.isna().any(axis=1), :].index - x_group_rolling = x_group_value.rolling(window=self.freq_str) + indices_nan = x_group_value.loc[ + x_group_value.isna().any(axis=1), : + ].index + x_group_rolling = x_group_value.rolling( + window=self.freq_str + ) for x_rolling in x_group_rolling: if x_rolling.index[-1] in indices_nan: x_windows.append(x_rolling) x_rolling_ = x_rolling.copy() for idx, col in enumerate(columns_index): x_rolling_[col] = x_group_index[idx] - x_rolling_ = x_rolling_.set_index(columns_index, append=True) - x_rolling_ = x_rolling_.reorder_levels(x.index.names) + x_rolling_ = x_rolling_.set_index( + columns_index, append=True + ) + x_rolling_ = x_rolling_.reorder_levels( + x.index.names + ) x_windows_indices.append(x_rolling_.index) else: - for x_w in x.resample(rule=self.freq_str, level=self.index_datetime): + for x_w in x.resample( + rule=self.freq_str, level=self.index_datetime + ): x_windows.append(x_w[1]) x_windows_indices.append(x_w[1].index) @@ -736,7 +853,12 @@ def _process_data( if x_w_shape[0] < self.size_window: npad = [(0, self.size_window - x_w_shape[0]), (0, 0)] x_w_norm = np.pad(x_w_norm, pad_width=npad, mode="wrap") - x_w_mask = np.pad(x_w_mask, pad_width=npad, mode="constant", constant_values=1) + x_w_mask = np.pad( + x_w_mask, + pad_width=npad, + mode="constant", + constant_values=1, + ) x_windows_processed.append(x_w_norm) x_windows_mask_processed.append(x_w_mask) @@ -750,10 +872,19 @@ def _process_data( x_m_shape = x_m.shape if x_m_shape[0] < self.size_window: npad = [(0, self.size_window - x_m_shape[0]), (0, 0)] - x_m_mask = np.pad(x_m_mask, pad_width=npad, mode="constant", constant_values=1) + x_m_mask = np.pad( + x_m_mask, + pad_width=npad, + mode="constant", + constant_values=1, + ) x_windows_mask_processed.append(x_m_mask) - return np.array(x_windows_processed), np.array(x_windows_mask_processed), x_windows_indices + return ( + np.array(x_windows_processed), + np.array(x_windows_mask_processed), + x_windows_indices, + ) def _process_reversely_data( self, x_imputed: np.ndarray, x_input: pd.DataFrame, x_indices: List @@ -766,9 +897,13 @@ def _process_reversely_data( x_indices_nan_only.append(x_indices_batch[imputed_index]) if len(np.shape(x_indices_nan_only)) == 1: - x_out_index = pd.Index(x_indices_nan_only, name=x_input.index.names[0]) + x_out_index = pd.Index( + x_indices_nan_only, name=x_input.index.names[0] + ) else: - x_out_index = pd.MultiIndex.from_tuples(x_indices_nan_only, names=x_input.index.names) + x_out_index = pd.MultiIndex.from_tuples( + x_indices_nan_only, names=x_input.index.names + ) x_normalized = self.normalizer_x.inverse_transform(x_imputed_nan_only) x_out = pd.DataFrame( x_normalized, @@ -796,8 +931,8 @@ def fit( cols_imputed: Tuple[str, ...] = (), index_datetime: str = "", freq_str: str = "1D", - ) -> Self: - """Fit data + ) -> "TsDDPM": + """Fit data. Parameters ---------- @@ -812,8 +947,8 @@ def fit( x_valid : pd.DataFrame, optional Dataframe for validation, by default None metrics_valid : Tuple[Callable, ...], optional - Set of validation metrics, by default ( metrics.mean_absolute_error, - metrics.dist_wasserstein ) + Set of validation metrics, by default (metrics.mean_absolute_error, + metrics.dist_wasserstein) round : int, optional Number of decimal places to round to, by default 10 cols_imputed : Tuple[str, ...], optional @@ -822,19 +957,23 @@ def fit( Name of datetime-like index freq_str : str Frequency string of DateOffset of Pandas + Raises ------ ValueError Batch size is larger than data size + Returns ------- Self Return Self + """ if index_datetime == "": raise ValueError( - "Please set the params index_datetime (the name of datatime-like index column)." - + f" Suggestions: {x.index.names}" + "Please set the params index_datetime " + "(the name of datatime-like index column). " + f" Suggestions: {x.index.names}" ) self.index_datetime = index_datetime self.freq_str = freq_str diff --git a/qolmat/imputations/diffusions/utils.py b/qolmat/imputations/diffusions/utils.py index c67a2f5f..eb24fb4f 100644 --- a/qolmat/imputations/diffusions/utils.py +++ b/qolmat/imputations/diffusions/utils.py @@ -1,9 +1,11 @@ +"""Utils for diffusion imputers.""" + import numpy as np import torch def get_num_params(model: torch.nn.Module) -> int: - """Get the total number of parameters of a model + """Get the total number of parameters of a model. Parameters ---------- @@ -14,6 +16,7 @@ def get_num_params(model: torch.nn.Module) -> int: ------- float the total number of parameters + """ model_parameters = filter(lambda p: p.requires_grad, model.parameters()) params = sum([np.prod(p.size()) for p in model_parameters]) diff --git a/qolmat/imputations/em_sampler.py b/qolmat/imputations/em_sampler.py index 463add50..eba85062 100644 --- a/qolmat/imputations/em_sampler.py +++ b/qolmat/imputations/em_sampler.py @@ -1,6 +1,8 @@ +"""Script for EM imputation.""" + +import warnings from abc import abstractmethod from typing import Dict, List, Literal, Tuple, Union -import warnings import numpy as np from numpy.typing import NDArray @@ -8,15 +10,17 @@ from scipy import optimize as spo from sklearn import utils as sku from sklearn.base import BaseEstimator, TransformerMixin -from typing_extensions import Self +# from typing_extensions import Self from qolmat.utils import utils def _conjugate_gradient(A: NDArray, X: NDArray, mask: NDArray) -> NDArray: - """ - Minimize Tr(X.T AX) wrt X where X is constrained to the initial value outside the given mask - To this aim, we compute in parallel a gradient algorithm for each row. + """Compute conjugate gradient. + + Minimize Tr(X.T AX) wrt X where X is constrained to the initial value + outside the given mask To this aim, we compute in parallel a gradient + algorithm for each row. Parameters ---------- @@ -25,12 +29,14 @@ def _conjugate_gradient(A: NDArray, X: NDArray, mask: NDArray) -> NDArray: X : NDArray Array containing the values to optimize mask : NDArray - Boolean array indicating if a value of X is a variable of the optimization + Boolean array indicating if a value of X is a variable of + the optimization Returns ------- NDArray Minimized array. + """ rows_imputed = mask.any(axis=1) X_temp = X[rows_imputed, :].copy() @@ -44,7 +50,7 @@ def _conjugate_gradient(A: NDArray, X: NDArray, mask: NDArray) -> NDArray: alphan = np.zeros(n_rows) betan = np.zeros(n_rows) for n in range(n_iter + 2): - # if np.max(np.sum(rn**2)) < tolerance : # Condition de sortie " usuelle " + # if np.max(np.sum(rn**2)) < tolerance : # X_temp[mask_isna] = xn[mask_isna] # return X_temp.transpose() Apn = pn @ A @@ -53,14 +59,18 @@ def _conjugate_gradient(A: NDArray, X: NDArray, mask: NDArray) -> NDArray: denominator = np.sum(pn * Apn, axis=1) not_converged = denominator != 0 # we stop updating if convergence is reached for this row - alphan[not_converged] = numerator[not_converged] / denominator[not_converged] + alphan[not_converged] = ( + numerator[not_converged] / denominator[not_converged] + ) xn, rnp1 = xn + pn * alphan[:, None], rn - Apn * alphan[:, None] numerator = np.sum(rnp1**2, axis=1) denominator = np.sum(rn**2, axis=1) not_converged = denominator != 0 # we stop updating if convergence is reached for this row - betan[not_converged] = numerator[not_converged] / denominator[not_converged] + betan[not_converged] = ( + numerator[not_converged] / denominator[not_converged] + ) pn, rn = rnp1 + pn * betan[:, None], rnp1 @@ -71,8 +81,12 @@ def _conjugate_gradient(A: NDArray, X: NDArray, mask: NDArray) -> NDArray: return X_final -def max_diff_Linf(list_params: List[NDArray], n_steps: int, order: int = 1) -> float: - """Computes the maximal L infinity norm between the `n_steps` last elements spaced by order. +def max_diff_Linf( + list_params: List[NDArray], n_steps: int, order: int = 1 +) -> float: + """Compute the maximal L infinity norm. + + Computed between the `n_steps` last elements spaced by order. Used to compute the stop criterion. Parameters @@ -88,6 +102,7 @@ def max_diff_Linf(list_params: List[NDArray], n_steps: int, order: int = 1) -> f ------- float Minimal norm of differences + """ params = np.stack(list_params[-n_steps - order : -order]) params_shift = np.stack(list_params[-n_steps:]) @@ -96,8 +111,9 @@ def max_diff_Linf(list_params: List[NDArray], n_steps: int, order: int = 1) -> f class EM(BaseEstimator, TransformerMixin): - """ - Generic abstract class for missing values imputation through EM optimization and + """Abstract class for EM imputatoin. + + It uses imputation through EM optimization and a projected MCMC sampling process. Parameters @@ -110,30 +126,35 @@ class EM(BaseEstimator, TransformerMixin): Number of iterations for the Gibbs sampling method (+ noise addition), necessary for convergence, by default 50. n_samples : int, optional - Number of data samples used to estimate the parameters of the distribution. Default, 10 + Number of data samples used to estimate the parameters of the + distribution. Default, 10 ampli : float, optional Whether to sample the posterior (1) or to maximise likelihood (0), by default 1. random_state : int, optional - The seed of the pseudo random number generator to use, for reproductibility. + The seed of the pseudo random number generator to use, + for reproductibility. dt : float, optional - Process integration time step, a large value increases the sample bias and can make - the algorithm unstable, but compensates for a smaller n_iter_ou. By default, 2e-2. + Process integration time step, a large value increases the sample bias + and can make the algorithm unstable, but compensates for a + smaller n_iter_ou. By default, 2e-2. tolerance : float, optional - Threshold below which a L infinity norm difference indicates the convergence of the - parameters - stagnation_threshold : float, optional - Threshold below which a stagnation of the L infinity norm difference indicates the + Threshold below which a L infinity norm difference indicates the convergence of the parameters + stagnation_threshold : float, optional + Threshold below which a stagnation of the L infinity norm difference + indicates the convergence of the parameters stagnation_loglik : float, optional - Threshold below which an absolute difference of the log likelihood indicates the - convergence of the parameters + Threshold below which an absolute difference of the log likelihood + indicates the convergence of the parameters min_std: float, optional - Threshold below which the initial data matrix is considered ill-conditioned + Threshold below which the initial data matrix is considered + ill-conditioned period : int, optional Integer used to fold the temporal data periodically verbose : bool, optional Verbosity level, if False the warnings are silenced + """ def __init__( @@ -153,7 +174,10 @@ def __init__( verbose: bool = False, ): if method not in ["mle", "sample"]: - raise ValueError(f"`method` must be 'mle' or 'sample', provided value is '{method}'") + raise ValueError( + "`method` must be 'mle' or 'sample', " + f"provided value is '{method}'." + ) self.method = method self.max_iter_em = max_iter_em @@ -180,38 +204,73 @@ def _check_convergence(self) -> bool: @abstractmethod def reset_learned_parameters(self): + """Reset learned parameters.""" pass @abstractmethod def update_parameters(self, X: NDArray): + """Update parameters.""" pass @abstractmethod def combine_parameters(self): + """Combine parameters.""" pass def fit_parameters(self, X: NDArray): + """Fir parameters. + + Parameters + ---------- + __________ + X: NDArray + Array to compute the parameters. + + """ self.reset_learned_parameters() self.update_parameters(X) self.combine_parameters() def fit_parameters_with_missingness(self, X: NDArray): - """ - First estimation of the model parameters based on data with missing values. + """Fit the first estimation of the model parameters. + + It is based on data with missing values. Parameters ---------- X : NDArray Data matrix with missingness + """ X_imp = self.init_imputation(X) self.fit_parameters(X_imp) def update_criteria_stop(self, X: NDArray): + """Update the stopping criteria based on X. + + Parameters + ---------- + X : NDArray + array used to compute log likelihood. + + """ self.loglik = self.get_loglikelihood(X) @abstractmethod def get_loglikelihood(self, X: NDArray) -> float: + """Compute the loglikelihood of an array. + + Parameters + ---------- + X : NDArray + Input array. + + Returns + ------- + float + log-likelihood. + + """ return 0 @abstractmethod @@ -219,10 +278,24 @@ def gradient_X_loglik( self, X: NDArray, ) -> NDArray: + """Compute the gradient X loglik. + + Parameters + ---------- + X : NDArray + input array + + Returns + ------- + NDArray + gradient + + """ return np.empty # type: ignore #noqa def get_gamma(self, n_cols: int) -> NDArray: - """ + """Get gamma. + Normalization matrix in the sampling process. Parameters @@ -234,6 +307,7 @@ def get_gamma(self, n_cols: int) -> NDArray: ------- NDArray Gamma matrix + """ # return np.ones((1, n_cols)) return np.eye(n_cols) @@ -246,13 +320,14 @@ def _maximize_likelihood(self, X: NDArray, mask_na: NDArray) -> NDArray: X : NDArray Input numpy array without missingness mask_na : NDArray - Boolean dataframe indicating which coefficients should be resampled, and are therefore - the variables of the optimization + Boolean dataframe indicating which coefficients should be + resampled, and are therefore the variables of the optimization Returns ------- NDArray DataFrame with imputed values. + """ def fun_obj(x): @@ -267,7 +342,8 @@ def fun_jac(x): grad_x = grad_x[mask_na] return grad_x - # the method BFGS is much slower, probabily not adapted to the high-dimension setting + # the method BFGS is much slower, probabily not adapted + # to the high-dimension setting res = spo.minimize(fun_obj, X[mask_na], jac=fun_jac, method="CG") x = res.x @@ -281,27 +357,31 @@ def _sample_ou( mask_na: NDArray, estimate_params: bool = True, ) -> NDArray: - """ - Samples the Gaussian distribution under the constraint that not na values must remain + """Sample the Gaussian distribution. + + Under the constraint that not na values must remain unchanged, using a projected Ornstein-Uhlenbeck process. - The sampled distribution tends to the target distribution in the limit dt -> 0 and - n_iter_ou x dt -> infty. + The sampled distribution tends to the target distribution + in the limit dt -> 0 and n_iter_ou x dt -> infty. Parameters ---------- - df : NDArray - Inital dataframe to be imputed, which should have been already imputed using a simple - method. This first imputation will be used as an initial guess. + X : NDArray + Inital dataframe to be imputed, which should have been already + imputed using a simple method. This first imputation will be used + as an initial guess. mask_na : NDArray - Boolean dataframe indicating which coefficients should be resampled. + Boolean dataframe indicating which coefficients should be + resampled. estimate_params : bool - Indicates if the parameters of the distribution should be estimated while the data are - sampled. + Indicates if the parameters of the distribution should be estimated + while the data are sampled. Returns ------- NDArray Sampled data matrix + """ X_copy = X.copy() n_rows, n_cols = X_copy.shape @@ -314,7 +394,10 @@ def _sample_ou( for i in range(self.n_iter_ou): noise = self.ampli * self.rng.normal(0, 1, size=(n_rows, n_cols)) grad_X = -self.gradient_X_loglik(X_copy) - X_copy += -self.dt * grad_X @ gamma + np.sqrt(2 * self.dt) * noise @ sqrt_gamma + X_copy += ( + -self.dt * grad_X @ gamma + + np.sqrt(2 * self.dt) * noise @ sqrt_gamma + ) X_copy[~mask_na] = X_init[~mask_na] if estimate_params: self.update_parameters(X_copy) @@ -322,6 +405,14 @@ def _sample_ou( return X_copy def fit_X(self, X: NDArray) -> None: + """Ft X array. + + Parameters + ---------- + X : NDArray + Input array. + + """ mask_na = np.isnan(X) # first imputation @@ -351,14 +442,14 @@ def fit_X(self, X: NDArray) -> None: self.dict_criteria_stop = {key: [] for key in self.dict_criteria_stop} self.X = X - def fit(self, X: NDArray) -> Self: - """ - Fit the statistical distribution with the input X array. + def fit(self, X: NDArray) -> "EM": + """Fit the statistical distribution with the input X array. Parameters ---------- X : NDArray Numpy array to be imputed + """ X = X.copy() self.shape_original = X.shape @@ -394,8 +485,7 @@ def fit(self, X: NDArray) -> Self: return self def transform(self, X: NDArray) -> NDArray: - """ - Transform the input X array by imputing the missing values. + """Transform the input X array by imputing the missing values. Parameters ---------- @@ -406,6 +496,7 @@ def transform(self, X: NDArray) -> NDArray: ------- NDArray Final array after EM sampling. + """ mask_na = np.isnan(X) X = X.copy() @@ -432,8 +523,7 @@ def transform(self, X: NDArray) -> NDArray: return X def pretreatment(self, X, mask_na) -> Tuple[NDArray, NDArray]: - """ - Pretreats the data before imputation by EM, making it more robust. + """Pretreat the data before imputation by EM, making it more robust. Parameters ---------- @@ -448,13 +538,15 @@ def pretreatment(self, X, mask_na) -> Tuple[NDArray, NDArray]: A tuple containing: - X the pretreatd data matrix - mask_na the updated mask + """ return X, mask_na def _check_conditionning(self, X: NDArray): - """ - Check that the data matrix X is not ill-conditioned. Running the EM algorithm on data with - colinear columns leads to numerical instability and unconsistent results. + """Check that the data matrix X is not ill-conditioned. + + Running the EM algorithm on data with colinear columns leads to + numerical instability and unconsistent results. Parameters ---------- @@ -465,6 +557,7 @@ def _check_conditionning(self, X: NDArray): ------ IllConditioned Data matrix is ill-conditioned due to colinear columns. + """ n_samples, n_cols = X.shape # if n_rows == 1 the function np.cov returns a float @@ -476,17 +569,20 @@ def _check_conditionning(self, X: NDArray): min_sv = min(np.sqrt(sv)) if min_sv < self.min_std: warnings.warn( - f"The covariance matrix is ill-conditioned, indicating high-colinearity: the " - f"smallest singular value of the data matrix is smaller than the threshold " - f"min_std ({min_sv} < {self.min_std}). Consider removing columns of decreasing " - f"the threshold." + "The covariance matrix is ill-conditioned, " + "indicating high-colinearity: the " + "smallest singular value of the data matrix is smaller " + "than the threshold " + f"min_std ({min_sv} < {self.min_std}). " + "Consider removing columns of decreasing the threshold." ) class MultiNormalEM(EM): - """ - Imputation of missing values using a multivariate Gaussian model through EM optimization and - using a projected Ornstein-Uhlenbeck process. + """Multinormal EM imputer. + + Imputation of missing values using a multivariate Gaussian model through + EM optimization and using a projected Ornstein-Uhlenbeck process. Parameters ---------- @@ -498,28 +594,32 @@ class MultiNormalEM(EM): Number of iterations for the Gibbs sampling method (+ noise addition), necessary for convergence, by default 50. n_samples : int, optional - Number of data samples used to estimate the parameters of the distribution. Default, 10 + Number of data samples used to estimate the parameters of the + distribution. Default, 10 ampli : float, optional Whether to sample the posterior (1) or to maximise likelihood (0), by default 1. random_state : int, optional - The seed of the pseudo random number generator to use, for reproductibility. + The seed of the pseudo random number generator to use, + for reproductibility. dt : float - Process integration time step, a large value increases the sample bias and can make - the algorithm unstable, but compensates for a smaller n_iter_ou. By default, 2e-2. + Process integration time step, a large value increases the sample bias + and can make the algorithm unstable, but compensates for a + smaller n_iter_ou. By default, 2e-2. tolerance : float, optional - Threshold below which a L infinity norm difference indicates the convergence of the - parameters + Threshold below which a L infinity norm difference indicates the + convergence of the parameters stagnation_threshold : float, optional - Threshold below which a L infinity norm difference indicates the convergence of the - parameters - stagnation_loglik : float, optional - Threshold below which an absolute difference of the log likelihood indicates the + Threshold below which a L infinity norm difference indicates the convergence of the parameters + stagnation_loglik : float, optional + Threshold below which an absolute difference of the log likelihood + indicates the convergence of the parameters period : int, optional Integer used to fold the temporal data periodically verbose : bool, optional Verbosity level, if False the warnings are silenced + """ def __init__( @@ -554,9 +654,11 @@ def __init__( self.dict_criteria_stop = {"logliks": [], "means": [], "covs": []} def get_loglikelihood(self, X: NDArray) -> float: - """ - Value of the log-likelihood up to a constant for the provided X, using the attributes - `means` and `cov_inv` for the multivariate normal distribution. + """Get the log-likelihood. + + Value of the log-likelihood up to a constant for the provided X, + using the attributes `means` and `cov_inv` for the multivariate + normal distribution. Parameters ---------- @@ -567,13 +669,15 @@ def get_loglikelihood(self, X: NDArray) -> float: ------- float Computed value + """ Xc = X - self.means return -((Xc @ self.cov_inv) * Xc).sum().sum() / 2 def gradient_X_loglik(self, X: NDArray) -> NDArray: - """ - Gradient of the log-likelihood for the provided X, using the attributes + """Compute the gradient of the log-likelihood for the provided X. + + It uses the attributes `means` and `cov_inv` for the multivariate normal distribution. Parameters @@ -584,15 +688,19 @@ def gradient_X_loglik(self, X: NDArray) -> NDArray: Returns ------- NDArray - The gradient of the log-likelihood with respect to the input variable `X`. + The gradient of the log-likelihood with respect to the input + variable `X`. + """ grad_X = -(X - self.means) @ self.cov_inv return grad_X def get_gamma(self, n_cols: int) -> NDArray: - """ - If the covariance matrix is not full-rank, defines the projection matrix keeping the - sampling process in the relevant subspace. + """Get gamma. + + If the covariance matrix is not full-rank, defines the + projection matrix keeping the sampling process in the relevant + subspace. Parameters ---------- @@ -603,6 +711,7 @@ def get_gamma(self, n_cols: int) -> NDArray: ------- NDArray Gamma matrix + """ U, diag, Vt = spl.svd(self.cov) diag_trunc = np.where(diag < self.min_std**2, 0, diag) @@ -614,13 +723,13 @@ def get_gamma(self, n_cols: int) -> NDArray: return gamma def update_criteria_stop(self, X: NDArray): - """ - Updates the variables which will be used to compute the stop critera + """Update the variables to compute the stopping critera. Parameters ---------- X : NDArray Input matrix with variables in column + """ self.loglik = self.get_loglikelihood(X) self.dict_criteria_stop["means"].append(self.means) @@ -628,20 +737,18 @@ def update_criteria_stop(self, X: NDArray): self.dict_criteria_stop["logliks"].append(self.loglik) def reset_learned_parameters(self): - """ - Resets all lists of estimated parameters before starting a new estimation. - """ + """Reset lists of parameters before starting a new estimation.""" self.list_means = [] self.list_cov = [] def update_parameters(self, X): - """ - Retains statistics relative to the current sample, in prevision of combining them. + """Retain statistics relative to the current sample. Parameters ---------- X : NDArray Input matrix with variables in column + """ n_rows, n_cols = X.shape means = np.mean(X, axis=0) @@ -654,9 +761,9 @@ def update_parameters(self, X): self.list_cov.append(cov) def combine_parameters(self): - """ - Combine all statistics computed for each sample in the update step, using the MANOVA - formula. + """Combine all statistics computed for each sample in the update step. + + If uses the MANOVA formula. """ list_means = self.list_means[-self.n_samples :] list_cov = self.list_cov[-self.n_samples :] @@ -674,20 +781,21 @@ def combine_parameters(self): self.cov_inv = np.linalg.pinv(self.cov) def fit_parameters_with_missingness(self, X: NDArray): - """ - First estimation of the model parameters based on data with missing values. + """Fit the first estimation of the model parameters. + + It is based on data with missing values. Parameters ---------- X : NDArray Data matrix with missingness + """ self.means, self.cov = utils.nan_mean_cov(X) self.cov_inv = np.linalg.pinv(self.cov) def set_parameters(self, means: NDArray, cov: NDArray): - """ - Sets the model parameters from a user value. + """Set the model parameters from a user value. Parameters ---------- @@ -695,27 +803,28 @@ def set_parameters(self, means: NDArray, cov: NDArray): Specified value for the mean vector cov : NDArray Specified value for the covariance matrix + """ self.means = means self.cov = cov self.cov_inv = np.linalg.pinv(self.cov) def _maximize_likelihood(self, X: NDArray, mask_na: NDArray) -> NDArray: - """ - Get the argmax of a posterior distribution. + """Get the argmax of a posterior distribution. Parameters ---------- X : NDArray Input DataFrame without missingness mask_na : NDArray - Boolean dataframe indicating which coefficients should be resampled, and are therefore - the variables of the optimization + Boolean dataframe indicating which coefficients should be + resampled, and are therefore the variables of the optimization Returns ------- NDArray DataFrame with imputed values. + """ X_center = X - self.means X_imputed = _conjugate_gradient(self.cov_inv, X_center, mask_na) @@ -723,8 +832,7 @@ def _maximize_likelihood(self, X: NDArray, mask_na: NDArray) -> NDArray: return X_imputed def init_imputation(self, X: NDArray) -> NDArray: - """ - First simple imputation before iterating. + """First simple imputation before iterating. Parameters ---------- @@ -735,24 +843,29 @@ def init_imputation(self, X: NDArray) -> NDArray: ------- NDArray Imputed matrix + """ return utils.impute_nans(X, method="median") def _check_convergence(self) -> bool: - """ - Check if the EM algorithm has converged. Three criteria: - 1) if the differences between the estimates of the parameters (mean and covariance) is - less than a threshold (min_diff_reached - tolerance). - 2) if the difference of the consecutive differences of the estimates is less than a - threshold, i.e. stagnates over the last 5 interactions (min_diff_stable - - stagnation_threshold). + """Check if the EM algorithm has converged. + + Three criteria: + 1) if the differences between the estimates of the parameters + (mean and covariance) is less than a threshold + (min_diff_reached - tolerance). + 2) if the difference of the consecutive differences of the estimates + is less than a threshold, i.e. stagnates over the last 5 interactions + (min_diff_stable - stagnation_threshold). 3) if the likelihood of the data no longer increases, - i.e. stagnates over the last 5 iterations (max_loglik - stagnation_loglik). + i.e. stagnates over the last 5 iterations + (max_loglik - stagnation_loglik). Returns ------- bool True/False if the algorithm has converged + """ list_means = self.dict_criteria_stop["means"] list_covs = self.dict_criteria_stop["covs"] @@ -764,7 +877,10 @@ def _check_convergence(self) -> bool: min_diff_means1 = max_diff_Linf(list_means, n_steps=1) min_diff_covs1 = max_diff_Linf(list_covs, n_steps=1) - min_diff_reached = min_diff_means1 < self.tolerance and min_diff_covs1 < self.tolerance + min_diff_reached = ( + min_diff_means1 < self.tolerance + and min_diff_covs1 < self.tolerance + ) if min_diff_reached: return True @@ -789,9 +905,11 @@ def _check_convergence(self) -> bool: class VARpEM(EM): - """ - Imputation of missing values using a vector autoregressive model through EM optimization and - using a projected Ornstein-Uhlenbeck process. Equations and notations and from the following + """VAR(p) EM imputer. + + Imputation of missing values using a vector autoregressive model through + EM optimization and using a projected Ornstein-Uhlenbeck process. + Equations and notations and from the following reference, matrices are transposed for consistency: Lütkepohl (2005) New Introduction to Multiple Time Series Analysis @@ -810,19 +928,21 @@ class VARpEM(EM): Whether to sample the posterior (1) or to maximise likelihood (0), by default 1. random_state : int, optional - The seed of the pseudo random number generator to use, for reproductibility. + The seed of the pseudo random number generator to use, + for reproductibility. dt : float - Process integration time step, a large value increases the sample bias and can make - the algorithm unstable, but compensates for a smaller n_iter_ou. By default, 2e-2. + Process integration time step, a large value increases the sample bias + and can make the algorithm unstable, but compensates for + a smaller n_iter_ou. By default, 2e-2. tolerance : float, optional - Threshold below which a L infinity norm difference indicates the convergence of the - parameters + Threshold below which a L infinity norm difference indicates + the convergence of the parameters stagnation_threshold : float, optional - Threshold below which a L infinity norm difference indicates the convergence of the - parameters - stagnation_loglik : float, optional - Threshold below which an absolute difference of the log likelihood indicates the + Threshold below which a L infinity norm difference indicates the convergence of the parameters + stagnation_loglik : float, optional + Threshold below which an absolute difference of the log likelihood + indicates the convergence of the parameters period : int, optional Integer used to fold the temporal data periodically verbose: bool @@ -831,18 +951,19 @@ class VARpEM(EM): Attributes ---------- X_intermediate : list - List of pd.DataFrame giving the results of the EM process as function of the - iteration number. + List of pd.DataFrame giving the results of the EM process as function + of the iteration number. Examples -------- >>> import numpy as np >>> from qolmat.imputations.em_sampler import VARpEM >>> imputer = VARpEM(method="sample", random_state=11) - >>> X = np.array([[1, 1, 1, 1], - ... [np.nan, np.nan, 3, 2], - ... [1, 2, 2, 1], [2, 2, 2, 2]]) + >>> X = np.array( + ... [[1, 1, 1, 1], [np.nan, np.nan, 3, 2], [1, 2, 2, 1], [2, 2, 2, 2]] + ... ) >>> imputer.fit_transform(X) # doctest: +SKIP + """ def __init__( @@ -882,9 +1003,10 @@ def __init__( self.p_to_fit = True def get_loglikelihood(self, X: NDArray) -> float: - """ - Value of the log-likelihood up to a constant for the provided X, using the attributes - `nu`, `B` and `S` for the VAR(p) distribution. + """Get the log-likelihood. + + Value of the log-likelihood up to a constant for the provided X, + using the attributes `nu`, `B` and `S` for the VAR(p) distribution. Parameters ---------- @@ -895,15 +1017,17 @@ def get_loglikelihood(self, X: NDArray) -> float: ------- float Computed value + """ Z, Y = utils.create_lag_matrices(X, self.p) U = Y - Z @ self.B return -(U @ self.S_inv * U).sum().sum() / 2 def gradient_X_loglik(self, X: NDArray) -> NDArray: - """ - Gradient of the log-likelihood for the provided X, using the attributes - `means` and `cov_inv` for the VAR(p) distribution. + """Compute the gradient of the log-likelihood for the provided X. + + It uses the attributes `means` and `cov_inv` + for the VAR(p) distribution. Parameters ---------- @@ -913,7 +1037,9 @@ def gradient_X_loglik(self, X: NDArray) -> NDArray: Returns ------- NDArray - The gradient of the log-likelihood with respect to the input variable `X`. + The gradient of the log-likelihood with respect + to the input variable `X`. + """ n_rows, n_cols = X.shape Z, Y = utils.create_lag_matrices(X, p=self.p) @@ -928,9 +1054,11 @@ def gradient_X_loglik(self, X: NDArray) -> NDArray: return grad_1 + grad_2 def get_gamma(self, n_cols: int) -> NDArray: - """ - If the noise matrix is not full-rank, defines the projection matrix keeping the - sampling process in the relevant subspace. Rescales the process to avoid instabilities. + """Compue gamma. + + If the noise matrix is not full-rank, defines the projection matrix + keeping the sampling process in the relevant subspace. + Rescales the process to avoid instabilities. Parameters ---------- @@ -941,6 +1069,7 @@ def get_gamma(self, n_cols: int) -> NDArray: ------- NDArray Gamma matrix + """ U, diag, Vt = spl.svd(self.S) diag_trunc = np.where(diag < self.min_std**2, 0, diag) @@ -952,13 +1081,13 @@ def get_gamma(self, n_cols: int) -> NDArray: return gamma def update_criteria_stop(self, X: NDArray): - """ - Updates the variables which will be used to compute the stop critera + """Update the variable to compute the stopping critera. Parameters ---------- X : NDArray Input matrix with variables in column + """ self.loglik = self.get_loglikelihood(X) self.dict_criteria_stop["S"].append(self.list_S[-1]) @@ -966,9 +1095,7 @@ def update_criteria_stop(self, X: NDArray): self.dict_criteria_stop["logliks"].append(self.loglik) def reset_learned_parameters(self): - """ - Resets all lists of estimated parameters before starting a new estimation. - """ + """Reset lists of parameters before starting a new estimation.""" self.list_ZZ = [] self.list_ZY = [] self.list_B = [] @@ -976,15 +1103,14 @@ def reset_learned_parameters(self): self.list_YY = [] def update_parameters(self, X: NDArray) -> None: - """ - Retains statistics relative to the current sample, in prevision of combining them. + """Retain statistics relative to the current sample. Parameters ---------- X : NDArray Input matrix with variables in column - """ + """ Z, Y = utils.create_lag_matrices(X, self.p) n_obs = len(Z) ZZ = Z.T @ Z / n_obs @@ -1002,9 +1128,10 @@ def update_parameters(self, X: NDArray) -> None: self.list_YY.append(YY) def combine_parameters(self) -> None: - """ - Combine all statistics computed for each sample in the update step. The estimation of `nu` - and `B` corresponds to the MLE, whereas `S` is approximated. + """Combine statistics computed for each sample in the update step. + + The estimation of `nu` and `B` corresponds to the MLE, + whereas `S` is approximated. """ list_ZZ = self.list_ZZ[-self.n_samples :] list_ZY = self.list_ZY[-self.n_samples :] @@ -1018,28 +1145,32 @@ def combine_parameters(self) -> None: self.B = self.ZZ_inv @ self.ZY stack_YY = np.stack(list_YY) self.YY = np.mean(stack_YY, axis=0) - self.S = self.YY - self.ZY.T @ self.B - self.B.T @ self.ZY + self.B.T @ self.ZZ @ self.B + self.S = ( + self.YY + - self.ZY.T @ self.B + - self.B.T @ self.ZY + + self.B.T @ self.ZZ @ self.B + ) self.S[np.abs(self.S) < 1e-12] = 0 self.S_inv = np.linalg.pinv(self.S, rcond=1e-10) def set_parameters(self, B: NDArray, S: NDArray): - """ - Sets the model parameters from a user value. + """Set the model parameters from a user value. Parameters ---------- - means : NDArray + B : NDArray Specified value for the autoregression matrix S : NDArray Specified value for the noise covariance matrix + """ self.B = B self.S = S self.S_inv = np.linalg.pinv(self.S) def init_imputation(self, X: NDArray) -> NDArray: - """ - First simple imputation before iterating. + """First simple imputation before iterating. Parameters ---------- @@ -1050,14 +1181,16 @@ def init_imputation(self, X: NDArray) -> NDArray: ------- NDArray Imputed matrix + """ return utils.linear_interpolation(X) def pretreatment(self, X, mask_na) -> Tuple[NDArray, NDArray]: - """ - Pretreats the data before imputation by EM, making it more robust. In the case of the - VAR(p) model we freeze the naive imputation on the first observations if all variables are - missing to avoid explosive imputations. + """Pretreat the data before imputation by EM, making it more robust. + + In the case of the + VAR(p) model we freeze the naive imputation on the first observations + if all variables are missing to avoid explosive imputations. Parameters ---------- @@ -1072,6 +1205,7 @@ def pretreatment(self, X, mask_na) -> Tuple[NDArray, NDArray]: A tuple containing: - X the pretreatd data matrix - mask_na the updated mask + """ if self.p == 0: return X, mask_na @@ -1081,22 +1215,25 @@ def pretreatment(self, X, mask_na) -> Tuple[NDArray, NDArray]: return X, mask_na def _check_convergence(self) -> bool: - """ - Check if the EM algorithm has converged. Three criteria: - 1) if the differences between the estimates of the parameters (mean and covariance) is - less than a threshold (min_diff_reached - tolerance). - OR 2) if the difference of the consecutive differences of the estimates is less than a - threshold, i.e. stagnates over the last 5 interactions (min_diff_stable - - stagnation_threshold). + """Check if the EM algorithm has converged. + + Three criteria: + 1) if the differences between the estimates of the parameters + (mean and covariance) is less than a threshold + (min_diff_reached - tolerance). + OR 2) if the difference of the consecutive differences of the + estimates is less than a threshold, i.e. stagnates over the + last 5 interactions (min_diff_stable - stagnation_threshold). OR 3) if the likelihood of the data no longer increases, - i.e. stagnates over the last 5 iterations (max_loglik - stagnation_loglik). + i.e. stagnates over the last 5 iterations + (max_loglik - stagnation_loglik). Returns ------- bool True/False if the algorithm has converged - """ + """ list_B = self.dict_criteria_stop["B"] list_S = self.dict_criteria_stop["S"] list_logliks = self.dict_criteria_stop["logliks"] @@ -1107,7 +1244,9 @@ def _check_convergence(self) -> bool: min_diff_B1 = max_diff_Linf(list_B, n_steps=1) min_diff_S1 = max_diff_Linf(list_S, n_steps=1) - min_diff_reached = min_diff_B1 < self.tolerance and min_diff_S1 < self.tolerance + min_diff_reached = ( + min_diff_B1 < self.tolerance and min_diff_S1 < self.tolerance + ) if min_diff_reached: return True @@ -1118,7 +1257,8 @@ def _check_convergence(self) -> bool: min_diff_B5 = max_diff_Linf(list_B, n_steps=5) min_diff_S5 = max_diff_Linf(list_S, n_steps=5) min_diff_stable = ( - min_diff_B5 < self.stagnation_threshold and min_diff_S5 < self.stagnation_threshold + min_diff_B5 < self.stagnation_threshold + and min_diff_S5 < self.stagnation_threshold ) max_loglik5_ord1 = max_diff_Linf(list_logliks, n_steps=5, order=1) diff --git a/qolmat/imputations/imputers.py b/qolmat/imputations/imputers.py index a2550700..239788e6 100644 --- a/qolmat/imputations/imputers.py +++ b/qolmat/imputations/imputers.py @@ -1,54 +1,47 @@ +"""Script for the imputers.""" + import copy -from functools import partial import warnings -from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union -from typing_extensions import Self from abc import abstractmethod +from functools import partial +from typing import Any, Callable, Dict, Literal, Optional, Tuple, Union import numpy as np -from numpy.typing import NDArray -from scipy import sparse import pandas as pd import sklearn as skl +from numpy.typing import NDArray from sklearn import utils as sku -from sklearn.impute import SimpleImputer from sklearn.base import BaseEstimator -from sklearn.experimental import enable_iterative_imputer from sklearn.impute import IterativeImputer, KNNImputer from sklearn.impute._base import _BaseImputer -from sklearn.utils.validation import ( - _check_feature_names_in, - _num_samples, - check_array, - check_is_fitted, -) from statsmodels.tsa import seasonal as tsa_seasonal -from qolmat.imputations import em_sampler -from qolmat.imputations.rpca import rpca, rpca_noisy, rpca_pcp -from qolmat.imputations import softimpute +# from typing_extensions import Self +from qolmat.imputations import em_sampler, softimpute +from qolmat.imputations.rpca import rpca_noisy, rpca_pcp from qolmat.utils import utils -from qolmat.utils.exceptions import NotDataFrame, TypeNotHandled -from qolmat.utils.utils import HyperValue +from qolmat.utils.exceptions import NotDataFrame class _Imputer(_BaseImputer): - """ - Base class for all imputers. + """Base class for all imputers. Parameters ---------- columnwise : bool, optional - If True, the imputer will be computed for each column, else it will be computed on the - whole dataframe, by default False + If True, the imputer will be computed for each column, else it will be + computed on the whole dataframe, by default False shrink : bool, optional - Indicates if the elementwise imputation method returns a single value, by default False + Indicates if the elementwise imputation method returns a single value, + by default False random_state : Union[None, int, np.random.RandomState], optional Controls the randomness of the fit_transform, by default None imputer_params: Tuple[str, ...] - List of parameters of the imputer, which can be specified globally or columnwise + List of parameters of the imputer, which can be specified globally or + columnwise groups: Tuple[str, ...] List of column names to group by, by default [] + """ def __init__( @@ -67,9 +60,11 @@ def __init__( self.missing_values = np.nan def get_hyperparams(self, col: Optional[str] = None): - """ - Filter hyperparameters based on the specified column, the dictionary keys in the form - name_params/column are only relevent for the specified column and are filtered accordingly. + """Filter hyperparameters based on the specified column. + + The dictionary keys in the form + name_params/column are only relevent for the specified column and + are filtered accordingly. Parameters ---------- @@ -96,8 +91,7 @@ def get_hyperparams(self, col: Optional[str] = None): return hyperparams def _check_dataframe(self, X: NDArray): - """ - Checks that the input X is a dataframe, otherwise raises an error. + """Check that the input X is a dataframe, otherwise raises an error. Parameters ---------- @@ -108,32 +102,37 @@ def _check_dataframe(self, X: NDArray): ------ ValueError Input has to be a pandas.DataFrame. + """ if not isinstance(X, (pd.DataFrame)): raise NotDataFrame(type(X)) def _more_tags(self): - """ - This method indicates that this class allows inputs with categorical data and nans. It - modifies the behaviour of the functions checking data. - """ - return {"X_types": ["2darray", "categorical", "string"], "allow_nan": True} + """Indicate this class allows inputs with categorical data and nans. - def fit(self, X: pd.DataFrame, y=None) -> Self: + It modifies the behaviour of the functions checking data. """ - Fit the imputer on X. + return { + "X_types": ["2darray", "categorical", "string"], + "allow_nan": True, + } + + def fit(self, X: pd.DataFrame, y: pd.DataFrame = None) -> "_Imputer": + """Fit the imputer on X. Parameters ---------- X : pd.DataFrame Data matrix on which the Imputer must be fitted. + y : pd.DataFrame + None. Returns ------- self : Self Returns self. - """ + """ df = utils._validate_input(X) self.n_features_in_ = len(df.columns) @@ -143,11 +142,15 @@ def fit(self, X: pd.DataFrame, y=None) -> Self: self.columns_ = tuple(df.columns) self._rng = sku.check_random_state(self.random_state) - if hasattr(self, "estimator") and hasattr(self.estimator, "random_state"): + if hasattr(self, "estimator") and hasattr( + self.estimator, "random_state" + ): self.estimator.random_state = self._rng if self.groups: - self.ngroups_ = df.groupby(list(self.groups)).ngroup().rename("_ngroup") + self.ngroups_ = ( + df.groupby(list(self.groups)).ngroup().rename("_ngroup") + ) else: self.ngroups_ = pd.Series(0, index=df.index).rename("_ngroup") @@ -161,12 +164,14 @@ def fit(self, X: pd.DataFrame, y=None) -> Self: return self def transform(self, X: pd.DataFrame) -> pd.DataFrame: - """ - Returns a dataframe with same shape as `X`, unchanged values, where all nans are replaced - by non-nan values. Depending on the imputer parameters, the dataframe can be imputed with + """Transform/impute a dataframe. + + It retruns a dataframe with same shape as `X`, + unchanged values, where all nans are replaced by non-nan values. + Depending on the imputer parameters, the dataframe can be imputed with columnwise and/or groupwise methods. - Also works for numpy arrays, returning numpy arrays, but the use of pandas dataframe is - advised. + Also works for numpy arrays, returning numpy arrays, but the use of + pandas dataframe is advised. Parameters ---------- @@ -177,12 +182,13 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: ------- pd.DataFrame Imputed dataframe. - """ + """ df = utils._validate_input(X) if tuple(df.columns) != self.columns_: raise ValueError( - """The number of features is different from the counterpart in fit. + """The number of features is different + from the counterpart in fit. Reshape your data""" ) @@ -198,7 +204,9 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: if self.columnwise: df_imputed = df.copy() for col in cols_with_nans: - df_imputed[col] = self._transform_allgroups(df[[col]], col=col) + df_imputed[col] = self._transform_allgroups( + df[[col]], col=col + ) else: df_imputed = self._transform_allgroups(df) @@ -207,29 +215,35 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: return df_imputed - def fit_transform(self, X: pd.DataFrame, y=None) -> pd.DataFrame: - """ - Returns a dataframe with same shape as `X`, unchanged values, where all nans are replaced - by non-nan values. - Depending on the imputer parameters, the dataframe can be imputed with columnwise and/or - groupwise methods. + def fit_transform( + self, X: pd.DataFrame, y: pd.DataFrame = None + ) -> pd.DataFrame: + """Return a imputed dataframe. + + The retruned df has same shape as `X`, with unchanged values, + but all nans are replaced by non-nan values. + Depending on the imputer parameters, the dataframe can be imputed + with columnwise and/or groupwise methods. Parameters ---------- X : pd.DataFrame Dataframe to impute. + y : pd.DataFrame + None Returns ------- pd.DataFrame Imputed dataframe. + """ self.fit(X) return self.transform(X) def _fit_transform_fallback(self, df: pd.DataFrame) -> pd.DataFrame: - """ - Impute `df` by the median of each column if it still contains missing values. + """Impute `df` with each column's median if missing values remain. + This can introduce data leakage for forward imputers if unchecked. Parameters @@ -241,6 +255,7 @@ def _fit_transform_fallback(self, df: pd.DataFrame) -> pd.DataFrame: ------- pd.DataFrame Dataframe df imputed by the median of each column. + """ self._check_dataframe(df) cols_with_nan = df.columns[df.isna().any()] @@ -250,9 +265,12 @@ def _fit_transform_fallback(self, df: pd.DataFrame) -> pd.DataFrame: df[col] = df[col].fillna(df[col].mode()[0]) return df - def _fit_allgroups(self, df: pd.DataFrame, col: str = "__all__") -> Self: - """ - Fits the Imputer either on a column, for a columnwise setting, on or all columns. + def _fit_allgroups( + self, df: pd.DataFrame, col: str = "__all__" + ) -> "_Imputer": + """Fit the imputer. + + Either on a column, for a columnwise setting, on or all columns. Parameters ---------- @@ -270,8 +288,8 @@ def _fit_allgroups(self, df: pd.DataFrame, col: str = "__all__") -> Self: ------ ValueError Input has to be a pandas.DataFrame. - """ + """ self._check_dataframe(df) fun_on_col = partial(self._fit_element, col=col) if self.groups: @@ -283,16 +301,14 @@ def _fit_allgroups(self, df: pd.DataFrame, col: str = "__all__") -> Self: return self def _setup_fit(self) -> None: - """ - Setup step of the fit function, before looping over the columns. - """ - self._dict_fitting: Dict[str, Any] = dict() + """Set up step of the fit function, before looping over the columns.""" + self._dict_fitting: Dict[str, Any] = {} return - def _apply_groupwise(self, fun: Callable, df: pd.DataFrame, **kwargs) -> Any: - """ - Applies the function `fun`in a groupwise manner to the dataframe `df`. - + def _apply_groupwise( + self, fun: Callable, df: pd.DataFrame, **kwargs + ) -> Any: + """Apply the function `fun`in a groupwise manner to the dataframe `df`. Parameters ---------- @@ -300,11 +316,14 @@ def _apply_groupwise(self, fun: Callable, df: pd.DataFrame, **kwargs) -> Any: Function applied groupwise to the dataframe with arguments kwargs df : pd.DataFrame Dataframe on which the function is applied + **kwargs: dict + Additional arguments Returns ------- Any Depends on the function signature + """ self._check_dataframe(df) fun_on_col = partial(fun, **kwargs) @@ -317,11 +336,15 @@ def _apply_groupwise(self, fun: Callable, df: pd.DataFrame, **kwargs) -> Any: else: return fun_on_col(df) - def _transform_allgroups(self, df: pd.DataFrame, col: str = "__all__") -> pd.DataFrame: - """ - Impute `df` by applying the specialized method `transform_element` on each group, if - groups have been given. If the method leaves nan, `fit_transform_fallback` is called in - order to return a dataframe without nan. + def _transform_allgroups( + self, df: pd.DataFrame, col: str = "__all__" + ) -> pd.DataFrame: + """Impute `df`. + + It doe sit by applying the specialized method `transform_element` + on each group, if groups have been given. If the method leaves nan, + `fit_transform_fallback` is called in order to return a dataframe + without nan. Parameters ---------- @@ -339,10 +362,13 @@ def _transform_allgroups(self, df: pd.DataFrame, col: str = "__all__") -> pd.Dat ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) df = df.copy() - imputation_values = self._apply_groupwise(self._transform_element, df, col=col) + imputation_values = self._apply_groupwise( + self._transform_element, df, col=col + ) df = df.fillna(imputation_values) # fill na by applying imputation method without groups @@ -353,10 +379,13 @@ def _transform_allgroups(self, df: pd.DataFrame, col: str = "__all__") -> pd.Dat return df @abstractmethod - def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) -> Any: - """ - Fits the imputer on `df`, at the group and/or column level depending onself.groups and - self.columnwise. + def _fit_element( + self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 + ) -> Any: + """Fit the imputer on `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -376,6 +405,7 @@ def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) return self @@ -384,9 +414,10 @@ def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -406,14 +437,14 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) return df class ImputerOracle(_Imputer): - """ - Perfect imputer, requires to know real values. + """Perfect imputer, requires to know real values. Used as a reference to evaluate imputation metrics. @@ -423,6 +454,7 @@ class ImputerOracle(_Imputer): Dataframe containing real values. groups: Tuple[str, ...] List of column names to group by, by default [] + """ def __init__( @@ -431,38 +463,45 @@ def __init__( super().__init__() def set_solution(self, df: pd.DataFrame): - """Sets the true values to be returned by the oracle. + """Set the true values to be returned by the oracle. Parameters ---------- - X : pd.DataFrame + df : pd.DataFrame True dataset with mask + """ self.df_solution = df def transform(self, X: pd.DataFrame) -> pd.DataFrame: - """Impute df with corresponding known values + """Impute df with corresponding known values. Parameters ---------- - df : pd.DataFrame + X : pd.DataFrame dataframe to impute + Returns ------- pd.DataFrame dataframe imputed with premasked values + """ df = utils._validate_input(X) if tuple(df.columns) != self.columns_: raise ValueError( - """The number of features is different from the counterpart in fit. + """The number of features is different from + the counterpart in fit. Reshape your data""" ) if hasattr(self, "df_solution"): df_imputed = df.fillna(self.df_solution) else: - warnings.warn("OracleImputer not initialized! Returning imputation with zeros") + warnings.warn( + "OracleImputer not initialized! " + "Returning imputation with zeros" + ) df_imputed = df.fillna(0) if isinstance(X, (np.ndarray)): @@ -471,8 +510,10 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: class ImputerSimple(_Imputer): - """ - Impute each column by its mean, its median or its mode (if its categorical). + """Simple imputer. + + Impute each column by its mean, its median or its mode + (if its categorical). Parameters ---------- @@ -485,27 +526,37 @@ class ImputerSimple(_Imputer): >>> import pandas as pd >>> from qolmat.imputations import imputers >>> imputer = imputers.ImputerSimple() - >>> df = pd.DataFrame(data=[[1, 1, 1, 1], - ... [np.nan, np.nan, np.nan, np.nan], - ... [1, 2, 2, 5], - ... [2, 2, 2, 2]], - ... columns=["var1", "var2", "var3", "var4"]) + >>> df = pd.DataFrame( + ... data=[ + ... [1, 1, 1, 1], + ... [np.nan, np.nan, np.nan, np.nan], + ... [1, 2, 2, 5], + ... [2, 2, 2, 2], + ... ], + ... columns=["var1", "var2", "var3", "var4"], + ... ) >>> imputer.fit_transform(df) var1 var2 var3 var4 0 1.0 1.0 1.0 1.0 1 1.0 2.0 2.0 2.0 2 1.0 2.0 2.0 5.0 3 2.0 2.0 2.0 2.0 + """ - def __init__(self, groups: Tuple[str, ...] = (), strategy="median") -> None: + def __init__( + self, groups: Tuple[str, ...] = (), strategy="median" + ) -> None: super().__init__(groups=groups, columnwise=True, shrink=False) self.strategy = strategy - def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) -> Any: - """ - Fits the imputer on `df`, at the group and/or column level depending onself.groups and - self.columnwise. + def _fit_element( + self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 + ) -> Any: + """Fit the imputer on `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -525,6 +576,7 @@ def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) ------ NotDataFrame Input has to be a pandas.DataFrame. + """ if pd.api.types.is_numeric_dtype(df[col]): model = skl.impute.SimpleImputer(strategy=self.strategy) @@ -535,8 +587,9 @@ def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending on self.groups + """Transform the dataframe `df`. + + It does it at the group and/or column level depending onself.groups and self.columnwise. Parameters @@ -557,6 +610,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ model = self._dict_fitting[col][ngroup] X_imputed = model.fit_transform(df) @@ -564,8 +618,7 @@ def _transform_element( class ImputerShuffle(_Imputer): - """ - Impute using random samples from the considered column. + """Impute using random samples from the considered column. Parameters ---------- @@ -580,17 +633,22 @@ class ImputerShuffle(_Imputer): >>> import pandas as pd >>> from qolmat.imputations import imputers >>> imputer = imputers.ImputerShuffle(random_state=42) - >>> df = pd.DataFrame(data=[[1, 1, 1, 1], - ... [np.nan, np.nan, np.nan, np.nan], - ... [1, 2, 2, 5], - ... [2, 2, 2, 2]], - ... columns=["var1", "var2", "var3", "var4"]) + >>> df = pd.DataFrame( + ... data=[ + ... [1, 1, 1, 1], + ... [np.nan, np.nan, np.nan, np.nan], + ... [1, 2, 2, 5], + ... [2, 2, 2, 2], + ... ], + ... columns=["var1", "var2", "var3", "var4"], + ... ) >>> imputer.fit_transform(df) var1 var2 var3 var4 0 1.0 1.0 1.0 1.0 1 2.0 1.0 2.0 2.0 2 1.0 2.0 2.0 5.0 3 2.0 2.0 2.0 2.0 + """ def __init__( @@ -598,14 +656,17 @@ def __init__( groups: Tuple[str, ...] = (), random_state: Union[None, int, np.random.RandomState] = None, ) -> None: - super().__init__(groups=groups, columnwise=True, random_state=random_state) + super().__init__( + groups=groups, columnwise=True, random_state=random_state + ) def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -625,6 +686,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) n_missing = df.isna().sum().sum() @@ -640,9 +702,10 @@ def _transform_element( class ImputerLOCF(_Imputer): - """ - Impute by the last available value of the column. Relevent for time series. + """LOCF imputer. + It imputes by the last available value of the column. + Relevant for time series. If the first observations are missing, it is imputed by a NOCB Parameters @@ -656,17 +719,22 @@ class ImputerLOCF(_Imputer): >>> import pandas as pd >>> from qolmat.imputations import imputers >>> imputer = imputers.ImputerLOCF() - >>> df = pd.DataFrame(data=[[1, 1, 1, 1], - ... [np.nan, np.nan, np.nan, np.nan], - ... [1, 2, 2, 5], - ... [2, 2, 2, 2]], - ... columns=["var1", "var2", "var3", "var4"]) + >>> df = pd.DataFrame( + ... data=[ + ... [1, 1, 1, 1], + ... [np.nan, np.nan, np.nan, np.nan], + ... [1, 2, 2, 5], + ... [2, 2, 2, 2], + ... ], + ... columns=["var1", "var2", "var3", "var4"], + ... ) >>> imputer.fit_transform(df) var1 var2 var3 var4 0 1.0 1.0 1.0 1.0 1 1.0 1.0 1.0 1.0 2 1.0 2.0 2.0 5.0 3 2.0 2.0 2.0 2.0 + """ def __init__( @@ -678,9 +746,10 @@ def __init__( def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending on self.groups + and self.columnwise. Parameters ---------- @@ -700,6 +769,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) df_out = df.copy() @@ -709,7 +779,8 @@ def _transform_element( class ImputerNOCB(_Imputer): - """ + """NOCB imputer. + Impute by the next available value of the column. Relevent for time series. If the last observation is missing, it is imputed by a LOCF. @@ -724,17 +795,22 @@ class ImputerNOCB(_Imputer): >>> import pandas as pd >>> from qolmat.imputations import imputers >>> imputer = imputers.ImputerNOCB() - >>> df = pd.DataFrame(data=[[1, 1, 1, 1], - ... [np.nan, np.nan, np.nan, np.nan], - ... [1, 2, 2, 5], - ... [2, 2, 2, 2]], - ... columns=["var1", "var2", "var3", "var4"]) + >>> df = pd.DataFrame( + ... data=[ + ... [1, 1, 1, 1], + ... [np.nan, np.nan, np.nan, np.nan], + ... [1, 2, 2, 5], + ... [2, 2, 2, 2], + ... ], + ... columns=["var1", "var2", "var3", "var4"], + ... ) >>> imputer.fit_transform(df) var1 var2 var3 var4 0 1.0 1.0 1.0 1.0 1 1.0 2.0 2.0 5.0 2 1.0 2.0 2.0 5.0 3 2.0 2.0 2.0 2.0 + """ def __init__( @@ -746,9 +822,10 @@ def __init__( def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending on self.groups + and self.columnwise. Parameters ---------- @@ -768,6 +845,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) df_out = df.copy() @@ -777,10 +855,11 @@ def _transform_element( class ImputerInterpolation(_Imputer): - """ - This class implements a way to impute time series using some interpolation strategies - suppoted by pd.Series.interpolate, such as "linear", "slinear", "quadratic", ... - By default, linear interpolation. + """Interpolation imputer. + + This class implements a way to impute time series using some interpolation + strategies suppoted by pd.Series.interpolate, such as "linear", "slinear", + "quadratic", ... By default, linear interpolation. As for pd.Series.interpolate, if "method" is "spline" or "polynomial", an "order" has to be passed. @@ -789,14 +868,15 @@ class ImputerInterpolation(_Imputer): groups: Tuple[str, ...] List of column names to group by, by default [] method : Optional[str] = "linear" - name of the method for interpolation: "linear", "cubic", "spline", "slinear", ... - see pd.Series.interpolate for more example. + name of the method for interpolation: "linear", "cubic", "spline", + "slinear", ... see pd.Series.interpolate for more example. By default, the value is set to "linear". order : Optional[int] order for the spline interpolation col_time : Optional[str] - Name of the column representing the time index to use for the interpolation. If None, the - index is used assuming it is one-dimensional. + Name of the column representing the time index to use for the + interpolation. If None, the index is used assuming it + is one-dimensional. Examples -------- @@ -804,17 +884,22 @@ class ImputerInterpolation(_Imputer): >>> import pandas as pd >>> from qolmat.imputations import imputers >>> imputer = imputers.ImputerInterpolation(method="spline", order=2) - >>> df = pd.DataFrame(data=[[1, 1, 1, 1], - ... [np.nan, np.nan, np.nan, np.nan], - ... [1, 2, 2, 5], - ... [2, 2, 2, 2]], - ... columns=["var1", "var2", "var3", "var4"]) + >>> df = pd.DataFrame( + ... data=[ + ... [1, 1, 1, 1], + ... [np.nan, np.nan, np.nan, np.nan], + ... [1, 2, 2, 5], + ... [2, 2, 2, 2], + ... ], + ... columns=["var1", "var2", "var3", "var4"], + ... ) >>> imputer.fit_transform(df) var1 var2 var3 var4 0 1.000000 1.000000 1.000000 1.000000 1 0.666667 1.666667 1.666667 4.666667 2 1.000000 2.000000 2.000000 5.000000 3 2.000000 2.000000 2.000000 2.000000 + """ def __init__( @@ -824,7 +909,9 @@ def __init__( order: Optional[int] = None, col_time: Optional[str] = None, ) -> None: - super().__init__(imputer_params=("method", "order"), groups=groups, columnwise=True) + super().__init__( + imputer_params=("method", "order"), groups=groups, columnwise=True + ) self.method = method self.order = order self.col_time = col_time @@ -832,9 +919,10 @@ def __init__( def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending on self.groups + and self.columnwise. Parameters ---------- @@ -854,6 +942,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) hyperparams = self.get_hyperparams(col=col) @@ -869,10 +958,11 @@ def _transform_element( class ImputerResiduals(_Imputer): - """ + """Residual imputer. + This class implements an imputation method based on a STL decomposition. - The series are de-seasonalised, de-trended, residuals are imputed, then residuals are - re-seasonalised and re-trended. + The series are de-seasonalised, de-trended, residuals are imputed, + then residuals are re-seasonalised and re-trended. Parameters ---------- @@ -883,7 +973,8 @@ class ImputerResiduals(_Imputer): the index of x does not have a frequency. Overrides default periodicity of x if x is a pandas object with a timeseries index. model_tsa : Optional[str] - Type of seasonal component "additive" or "multiplicative". Abbreviations are accepted. + Type of seasonal component "additive" or "multiplicative". + Abbreviations are accepted. By default, the value is set to "additive" extrapolate_trend : int or 'freq', optional If set to > 0, the trend resulting from the convolution is @@ -900,15 +991,20 @@ class ImputerResiduals(_Imputer): >>> import pandas as pd >>> from qolmat.imputations.imputers import ImputerResiduals >>> np.random.seed(100) - >>> df = pd.DataFrame(index=pd.date_range('2015-01-01','2020-01-01')) + >>> df = pd.DataFrame(index=pd.date_range("2015-01-01", "2020-01-01")) >>> mean = 5 >>> offset = 10 - >>> df['y'] = np.cos(df.index.dayofyear/365*2*np.pi - np.pi)*mean + offset + >>> df["y"] = ( + ... np.cos(df.index.dayofyear / 365 * 2 * np.pi - np.pi) * mean + ... + offset + ... ) >>> trend = 5 - >>> df['y'] = df['y'] + trend*np.arange(0,df.shape[0])/df.shape[0] + >>> df["y"] = df["y"] + trend * np.arange(0, df.shape[0]) / df.shape[0] >>> noise_mean = 0 >>> noise_var = 2 - >>> df['y'] = df['y'] + np.random.normal(noise_mean, noise_var, df.shape[0]) + >>> df["y"] = df["y"] + np.random.normal( + ... noise_mean, noise_var, df.shape[0] + ... ) >>> mask = np.random.choice([True, False], size=df.shape) >>> df = df.mask(mask) >>> imputor = ImputerResiduals(period=365, model_tsa="additive") @@ -927,6 +1023,7 @@ class ImputerResiduals(_Imputer): 2020-01-01 12.780517 [1827 rows x 1 columns] + """ def __init__( @@ -955,9 +1052,10 @@ def __init__( def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending on self.groups + and self.columnwise. Parameters ---------- @@ -977,13 +1075,16 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) hyperparams = self.get_hyperparams(col=col) name = df.columns[0] values = df[df.columns[0]] values_interp = ( - values.interpolate(method=hyperparams["method_interpolation"]).ffill().bfill() + values.interpolate(method=hyperparams["method_interpolation"]) + .ffill() + .bfill() ) result = tsa_seasonal.seasonal_decompose( values_interp, @@ -996,15 +1097,18 @@ def _transform_element( residuals[values.isna()] = np.nan residuals = ( - residuals.interpolate(method=hyperparams["method_interpolation"]).ffill().bfill() + residuals.interpolate(method=hyperparams["method_interpolation"]) + .ffill() + .bfill() + ) + df_result = pd.DataFrame( + {name: result.seasonal + result.trend + residuals} ) - df_result = pd.DataFrame({name: result.seasonal + result.trend + residuals}) return df_result class ImputerKNN(_Imputer): - """ - This class implements an imputation by the k-nearest neighbors. + """K-nnearest neighbors imputer. Parameters ---------- @@ -1029,17 +1133,22 @@ class ImputerKNN(_Imputer): >>> import pandas as pd >>> from qolmat.imputations import imputers >>> imputer = imputers.ImputerKNN(n_neighbors=2) - >>> df = pd.DataFrame(data=[[1, 1, 1, 1], - ... [np.nan, np.nan, np.nan, np.nan], - ... [1, 2, 2, 5], - ... [2, 2, 2, 2]], - ... columns=["var1", "var2", "var3", "var4"]) + >>> df = pd.DataFrame( + ... data=[ + ... [1, 1, 1, 1], + ... [np.nan, np.nan, np.nan, np.nan], + ... [1, 2, 2, 5], + ... [2, 2, 2, 2], + ... ], + ... columns=["var1", "var2", "var3", "var4"], + ... ) >>> imputer.fit_transform(df) var1 var2 var3 var4 0 1.000000 1.000000 1.000000 1.000000 1 1.333333 1.666667 1.666667 2.666667 2 1.000000 2.000000 2.000000 5.000000 3 2.000000 2.000000 2.000000 2.000000 + """ def __init__( @@ -1049,15 +1158,20 @@ def __init__( weights: str = "distance", ) -> None: super().__init__( - imputer_params=("n_neighbors", "weights"), groups=groups, columnwise=False + imputer_params=("n_neighbors", "weights"), + groups=groups, + columnwise=False, ) self.n_neighbors = n_neighbors self.weights = weights - def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) -> KNNImputer: - """ - Fits the imputer on `df`, at the group and/or column level depending onself.groups and - self.columnwise. + def _fit_element( + self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 + ) -> KNNImputer: + """Fit. the imputer on `df`. + + It does it at the group and/or column level depending on self.groups + and self.columnwise. Parameters ---------- @@ -1077,9 +1191,13 @@ def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) - assert col == "__all__" + if col != "__all__": + raise ValueError( + f"col must be '__all__', but '{col}' has been passed." + ) hyperparameters = self.get_hyperparams() model = KNNImputer(metric="nan_euclidean", **hyperparameters) model = model.fit(df) @@ -1088,9 +1206,10 @@ def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending on self.groups + and self.columnwise. Parameters ---------- @@ -1110,31 +1229,37 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) - assert col == "__all__" + if col != "__all__": + raise ValueError( + f"col must be '__all__', but '{col}' has been passed." + ) model = self._dict_fitting["__all__"][ngroup] X_imputed = model.fit_transform(df) return pd.DataFrame(data=X_imputed, columns=df.columns, index=df.index) class ImputerMICE(_Imputer): - """ - Wrapper of the class sklearn.impute.IterativeImputer in our framework. This imputer relies - on a estimator which is iteratively + """MICE imputer. + + Wrapper of the class sklearn.impute.IterativeImputer in our framework. + This imputer relies on a estimator which is iterative. Parameters ---------- groups : Tuple[str, ...], optional - _description_, by default () + specific groups for groupby, by default () estimator : Optional[BaseEstimator], optional - _description_, by default None + estimator to use, by default None random_state : Union[None, int, np.random.RandomState], optional - _description_, by default None + random state, by default None sample_posterior : bool, optional - _description_, by default False + true if sample, false otherwise, by default False max_iter : int, optional - _description_, by default 100 + maximum number of iterations, by default 100 + """ def __init__( @@ -1158,9 +1283,10 @@ def __init__( def _fit_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> IterativeImputer: - """ - Fits the imputer on `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Fit the imputer on `df`. + + It does it at the group and/or column level depending on self.groups + and self.columnwise. Parameters ---------- @@ -1180,9 +1306,13 @@ def _fit_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) - assert col == "__all__" + if col != "__all__": + raise ValueError( + f"col must be '__all__', but '{col}' has been passed." + ) hyperparameters = self.get_hyperparams() model = IterativeImputer(estimator=self.estimator, **hyperparameters) model = model.fit(df) @@ -1192,8 +1322,9 @@ def _fit_element( def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending on self.groups + """Transform the dataframe `df`. + + It does it at the group and/or column level depending on self.groups and self.columnwise. Parameters @@ -1214,20 +1345,24 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. - """ + """ self._check_dataframe(df) - assert col == "__all__" + if col != "__all__": + raise ValueError( + f"col must be '__all__', but '{col}' has been passed." + ) model = self._dict_fitting["__all__"][ngroup] X_imputed = model.fit_transform(df) return pd.DataFrame(data=X_imputed, columns=df.columns, index=df.index) class ImputerRegressor(_Imputer): - """ + """Regressor imputer. + This class implements a regression imputer in the multivariate case. - It imputes each column using a single fit-predict for a given estimator, based on the colunms - which have no missing values. + It imputes each column using a single fit-predict for a given estimator, + based on the colunms which have no missing values. Parameters ---------- @@ -1238,8 +1373,8 @@ class ImputerRegressor(_Imputer): handler_nan : str Can be `fit, `row` or `column`: - if `fit`, the estimator is assumed to be robust to missing values - - if `row` all non complete rows will be removed from the train dataset, and will not be - used for the inferance, + - if `row` all non complete rows will be removed from the + train dataset, and will not be used for the inference, - if `column` all non complete columns will be ignored. By default, `row` random_state : Union[None, int, np.random.RandomState], optional @@ -1252,17 +1387,22 @@ class ImputerRegressor(_Imputer): >>> from qolmat.imputations import imputers >>> from sklearn.ensemble import ExtraTreesRegressor >>> imputer = imputers.ImputerRegressor(estimator=ExtraTreesRegressor()) - >>> df = pd.DataFrame(data=[[1, 1, 1, 1], - ... [np.nan, np.nan, np.nan, np.nan], - ... [1, 2, 2, 5], - ... [2, 2, 2, 2]], - ... columns=["var1", "var2", "var3", "var4"]) + >>> df = pd.DataFrame( + ... data=[ + ... [1, 1, 1, 1], + ... [np.nan, np.nan, np.nan, np.nan], + ... [1, 2, 2, 5], + ... [2, 2, 2, 2], + ... ], + ... columns=["var1", "var2", "var3", "var4"], + ... ) >>> imputer.fit_transform(df) var1 var2 var3 var4 0 1.0 1.0 1.0 1.0 1 1.0 2.0 2.0 2.0 2 1.0 2.0 2.0 5.0 3 2.0 2.0 2.0 2.0 + """ def __init__( @@ -1288,7 +1428,29 @@ def _predict_estimator(self, estimator, X) -> pd.Series: pred = estimator.predict(X) return pd.Series(pred, index=X.index) - def get_Xy_valid(self, df: pd.DataFrame, col: str) -> Tuple[pd.DataFrame, pd.Series]: + def get_Xy_valid( + self, df: pd.DataFrame, col: str + ) -> Tuple[pd.DataFrame, pd.Series]: + """Get a valid couple (X,y). + + Parameters + ---------- + df : pd.DataFrame + Input dataframe + col : str + column name. + + Returns + ------- + Tuple[pd.DataFrame, pd.Series] + Valid X and y. + + Raises + ------ + ValueError + _description_ + + """ X = df.drop(columns=col, errors="ignore") if self.handler_nan == "none": pass @@ -1298,7 +1460,8 @@ def get_Xy_valid(self, df: pd.DataFrame, col: str) -> Tuple[pd.DataFrame, pd.Ser X = X.dropna(how="any", axis=1) else: raise ValueError( - f"Value '{self.handler_nan}' is not correct for argument `handler_nan'" + f"Value '{self.handler_nan}' is not correct " + "for argument `handler_nan'." ) # X = pd.get_dummies(X, prefix_sep="=") y = df.loc[X.index, col] @@ -1307,8 +1470,9 @@ def get_Xy_valid(self, df: pd.DataFrame, col: str) -> Tuple[pd.DataFrame, pd.Ser def _fit_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> Optional[BaseEstimator]: - """ - Fits the imputer on `df`, at the group and/or column level depending onself.groups and + """Fit the imputer on `df`. + + It does it at the group and/or column level depending onself.groups and self.columnwise. Parameters @@ -1329,13 +1493,18 @@ def _fit_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) - assert col == "__all__" + if col != "__all__": + raise ValueError( + f"col must be '__all__', but '{col}' has been passed." + ) cols_with_nans = df.columns[df.isna().any()] - dict_estimators: Dict[str, BaseEstimator] = dict() + dict_estimators: Dict[str, BaseEstimator] = {} for col in cols_with_nans: - # Selects only the valid values in the Train Set according to the chosen method + # Selects only the valid values in the Train Set according + # to the chosen method X, y = self.get_Xy_valid(df, col) # Selects only non-NaN values for the Test Set @@ -1343,7 +1512,8 @@ def _fit_element( X = X[~is_na] y = y[~is_na] - # Train the model according to an ML or DL method and after predict the imputation + # Train the model according to an ML or DL method and + # after predict the imputation if not X.empty: estimator = copy.deepcopy(self.estimator) dict_estimators[col] = self._fit_estimator(estimator, X, y) @@ -1354,9 +1524,10 @@ def _fit_element( def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -1376,9 +1547,13 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) - assert col == "__all__" + if col != "__all__": + raise ValueError( + f"col must be '__all__', but '{col}' has been passed." + ) df_imputed = df.copy() cols_with_nans = df.columns[df.isna().any()] @@ -1402,10 +1577,12 @@ def _transform_element( class ImputerRpcaPcp(_Imputer): - """ - This class implements the Robust Principal Component Analysis imputation with Principal - Component Pursuit. The imputation minimizes a loss function combining a low-rank criterium on - the dataframe and a L1 penalization on the residuals. + """PCP RPCA imputer. + + This class implements the Robust Principal Component Analysis imputation + with Principal Component Pursuit. The imputation minimizes a loss function + combining a low-rank criterium on the dataframe and a L1 penalization on + the residuals. Parameters ---------- @@ -1414,9 +1591,11 @@ class ImputerRpcaPcp(_Imputer): columnwise : bool For the RPCA method to be applied columnwise (with reshaping of each column into an array) - or to be applied directly on the dataframe. By default, the value is set to False. + or to be applied directly on the dataframe. + By default, the value is set to False. random_state : Union[None, int, np.random.RandomState], optional Controls the randomness of the fit_transform, by default None + """ def __init__( @@ -1452,13 +1631,13 @@ def __init__( self.verbose = verbose def get_model(self, **hyperparams) -> rpca_pcp.RpcaPcp: - """ - Get the underlying model of the imputer based on its attributes. + """Get the underlying model of the imputer based on its attributes. Returns ------- rpca.RPCA RPCA model to be used in the fit and transform methods. + """ hyperparams = { key: hyperparams[key] @@ -1469,16 +1648,19 @@ def get_model(self, **hyperparams) -> rpca_pcp.RpcaPcp: "tolerance", ] } - model = rpca_pcp.RpcaPcp(random_state=self._rng, verbose=self.verbose, **hyperparams) + model = rpca_pcp.RpcaPcp( + random_state=self._rng, verbose=self.verbose, **hyperparams + ) return model def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -1498,6 +1680,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) hyperparams = self.get_hyperparams() @@ -1521,16 +1704,20 @@ def _transform_element( A_final = utils.get_shape_original(A, X.shape) X_imputed = M_final + A_final - df_imputed = pd.DataFrame(X_imputed, index=df.index, columns=df.columns) + df_imputed = pd.DataFrame( + X_imputed, index=df.index, columns=df.columns + ) df_imputed = df.where(~df.isna(), df_imputed) return df_imputed class ImputerRpcaNoisy(_Imputer): - """ - This class implements the Robust Principal Component Analysis imputation with added noise. - The imputation minimizes a loss function combining a low-rank criterium on the dataframe and + """Noise RPCA imputer. + + This class implements the Robust Principal Component Analysis imputation + with added noise. The imputation minimizes a loss function combining + a low-rank criterium on the dataframe and a L1 penalization on the residuals. Parameters @@ -1540,9 +1727,11 @@ class ImputerRpcaNoisy(_Imputer): columnwise : bool For the RPCA method to be applied columnwise (with reshaping of each column into an array) - or to be applied directly on the dataframe. By default, the value is set to False. + or to be applied directly on the dataframe. + By default, the value is set to False. random_state : Union[None, int, np.random.RandomState], optional Controls the randomness of the fit_transform, by default None + """ def __init__( @@ -1593,15 +1782,14 @@ def __init__( self.verbose = verbose def get_model(self, **hyperparams) -> rpca_noisy.RpcaNoisy: - """ - Get the underlying model of the imputer based on its attributes. + """Get the underlying model of the imputer based on its attributes. Returns ------- rpca.RPCA RPCA model to be used in the fit and transform methods. - """ + """ hyperparams = { key: hyperparams[key] for key in [ @@ -1615,15 +1803,18 @@ def get_model(self, **hyperparams) -> rpca_noisy.RpcaNoisy: "norm", ] } - model = rpca_noisy.RpcaNoisy(random_state=self._rng, verbose=self.verbose, **hyperparams) + model = rpca_noisy.RpcaNoisy( + random_state=self._rng, verbose=self.verbose, **hyperparams + ) return model def _fit_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> Tuple[NDArray, NDArray, NDArray]: - """ - Fits the imputer on `df`, at the group and/or column level depending on self.groups and - self.columnwise. + """Fit the imputer on `df`. + + It does it at the group and/or column level depending on self.groups + and self.columnwise. Parameters ---------- @@ -1646,6 +1837,7 @@ def _fit_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) hyperparams = self.get_hyperparams() @@ -1667,9 +1859,10 @@ def _fit_element( def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -1689,6 +1882,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) hyperparams = self.get_hyperparams() @@ -1716,13 +1910,15 @@ def _transform_element( class ImputerSoftImpute(_Imputer): - """ - This class implements the Soft Impute method: + """SoftIMpute imputer. - Hastie, Trevor, et al. Matrix completion and low-rank SVD via fast alternating least squares. - The Journal of Machine Learning Research 16.1 (2015): 3367-3402. + This class implements the Soft Impute method: + Hastie, Trevor, et al. Matrix completion and low-rank SVD via fast + alternating least squares. The Journal of Machine Learning Research 16.1 + (2015): 3367-3402. - This imputation technique is less robust than the RPCA, although it can provide faster. + This imputation technique is less robust than the RPCA, + although it can provide faster. Parameters ---------- @@ -1731,9 +1927,11 @@ class ImputerSoftImpute(_Imputer): columnwise : bool For the RPCA method to be applied columnwise (with reshaping of each column into an array) - or to be applied directly on the dataframe. By default, the value is set to False. + or to be applied directly on the dataframe. + By default, the value is set to False. random_state : Union[None, int, np.random.RandomState], optional Controls the randomness of the fit_transform, by default None + """ def __init__( @@ -1769,13 +1967,13 @@ def __init__( self.verbose = verbose def get_model(self, **hyperparams) -> softimpute.SoftImpute: - """ - Get the underlying model of the imputer based on its attributes. + """Get the underlying model of the imputer based on its attributes. Returns ------- softimpute.SoftImpute Soft Impute model to be used in the transform method. + """ hyperparams = { key: hyperparams[key] @@ -1785,7 +1983,9 @@ def get_model(self, **hyperparams) -> softimpute.SoftImpute: "tolerance", ] } - model = softimpute.SoftImpute(random_state=self._rng, verbose=self.verbose, **hyperparams) + model = softimpute.SoftImpute( + random_state=self._rng, verbose=self.verbose, **hyperparams + ) return model @@ -1793,8 +1993,8 @@ def get_model(self, **hyperparams) -> softimpute.SoftImpute: # self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 # ) -> softimpute.SoftImpute: # """ - # Fits the imputer on `df`, at the group and/or column level depending on - # self.groups and self.columnwise. + # Fits the imputer on `df`, at the group and/or column level depending + # on self.groups and self.columnwise. # Parameters # ---------- @@ -1825,9 +2025,10 @@ def get_model(self, **hyperparams) -> softimpute.SoftImpute: def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -1847,6 +2048,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) hyperparams = self.get_hyperparams() @@ -1863,7 +2065,9 @@ def _transform_element( A_final = utils.get_shape_original(A, X.shape) X_imputed = M_final + A_final - df_imputed = pd.DataFrame(X_imputed, index=df.index, columns=df.columns) + df_imputed = pd.DataFrame( + X_imputed, index=df.index, columns=df.columns + ) df_imputed = df.where(~df.isna(), df_imputed) return df_imputed @@ -1871,33 +2075,42 @@ def _transform_element( def _more_tags(self): return { "_xfail_checks": { - "check_fit2d_1sample": "This test shouldn't be running at all!", - "check_fit2d_1feature": "This test shouldn't be running at all!", + "check_fit2d_1sample": ( + "This test shouldn't be running at all!" + ), + "check_fit2d_1feature": ( + "This test shouldn't be running at all!" + ), }, } class ImputerEM(_Imputer): - """ - This class implements an imputation method based on joint modelling and an inference using a - Expectation-Minimization algorithm. + """EM imputer. + + This class implements an imputation method based on joint modelling and + an inference using a Expectation-Minimization algorithm. Parameters ---------- groups: Tuple[str, ...] List of column names to group by, by default [] method : {'multinormal', 'VAR'}, default='multinormal' - Method defining the hypothesis made on the data distribution. Possible values: - - 'multinormal' : the data points a independent and uniformly distributed following a - multinormal distribution + Method defining the hypothesis made on the data distribution. + Possible values: + - 'multinormal' : the data points a independent and uniformly + distributed following a multinormal distribution - 'VAR' : the data is a time series modeled by a VAR(p) process columnwise : bool - If False, correlations between variables will be used, which is advised. - If True, each column is imputed independently. For the multinormal case each - value will be imputed by the mean up to a noise with fixed noise, for the VAR1 case the - imputation will be a noisy temporal interpolation. + If False, correlations between variables will be used, + which is advised. + If True, each column is imputed independently. For the multinormal case + each value will be imputed by the mean up to a noise with fixed noise, + for the VAR1 case the imputation will be a noisy temporal + interpolation. random_state : Union[None, int, np.random.RandomState], optional Controls the randomness of the fit_transform, by default None + """ def __init__( @@ -1954,6 +2167,7 @@ def get_model(self, **hyperparams) -> em_sampler.EM: ------- em_sampler.EM EM model to be used in the fit and transform methods. + """ if self.model == "multinormal": hyperparams.pop("p") @@ -1980,9 +2194,10 @@ def get_model(self, **hyperparams) -> em_sampler.EM: def _fit_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> em_sampler.EM: - """ - Fits the imputer on `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Fit the imputer on `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -2002,6 +2217,7 @@ def _fit_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) hyperparams = self.get_hyperparams() @@ -2012,9 +2228,10 @@ def _fit_element( def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -2034,6 +2251,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) @@ -2044,6 +2262,8 @@ def _transform_element( X = df.values.astype(float) X_imputed = model.transform(X) - df_transformed = pd.DataFrame(X_imputed, columns=df.columns, index=df.index) + df_transformed = pd.DataFrame( + X_imputed, columns=df.columns, index=df.index + ) return df_transformed diff --git a/qolmat/imputations/imputers_pytorch.py b/qolmat/imputations/imputers_pytorch.py index 1cf7d5d3..aff2b32f 100644 --- a/qolmat/imputations/imputers_pytorch.py +++ b/qolmat/imputations/imputers_pytorch.py @@ -1,15 +1,20 @@ -import pandas as pd -import numpy as np +"""Script for pytroch imputers.""" + +from typing import Any, Callable, Dict, List, Optional, Tuple, Union -from typing import Any, Callable, List, Optional, Tuple, Union, Dict -from typing_extensions import Self +import numpy as np +import pandas as pd from numpy.typing import NDArray -from sklearn.preprocessing import StandardScaler from sklearn.base import BaseEstimator +from sklearn.preprocessing import StandardScaler -from qolmat.imputations.imputers import _Imputer, ImputerRegressor -from qolmat.utils.exceptions import EstimatorNotDefined, PyTorchExtraNotInstalled +# from typing_extensions import Self from qolmat.benchmark import metrics +from qolmat.imputations.imputers import ImputerRegressor, _Imputer +from qolmat.utils.exceptions import ( + EstimatorNotDefined, + PyTorchExtraNotInstalled, +) try: import torch @@ -20,8 +25,10 @@ class ImputerRegressorPyTorch(ImputerRegressor): - """ - This class inherits from the class ImputerRegressor and allows for PyTorch regressors. + """Imputer regressor based on PyTorch. + + This class inherits from the class ImputerRegressor + and allows for PyTorch regressors. Parameters ---------- @@ -32,8 +39,8 @@ class ImputerRegressorPyTorch(ImputerRegressor): handler_nan : str Can be `fit, `row` or `column`: - if `fit`, the estimator is assumed to be fitted on parcelar data, - - if `row` all non complete rows will be removed from the train dataset, and will not be - used for the inferance, + - if `row` all non complete rows will be removed from the train + dataset, and will not be used for the inference, - if `column`all non complete columns will be ignored. By default, `row` epochs: int @@ -42,6 +49,7 @@ class ImputerRegressorPyTorch(ImputerRegressor): Learning rate hen fitting the autoencoder, by default 0.001 loss_fn: Callable Loss used when fitting the autoencoder, by default nn.L1Loss() + """ def __init__( @@ -63,12 +71,15 @@ def __init__( self.loss_fn = loss_fn self.estimator = estimator - def _fit_estimator(self, estimator: nn.Sequential, X: pd.DataFrame, y: pd.DataFrame) -> Any: - """ - Fit the PyTorch estimator using the provided input and target data. + def _fit_estimator( + self, estimator: nn.Sequential, X: pd.DataFrame, y: pd.DataFrame + ) -> Any: + """Fit the PyTorch estimator using the provided input and target data. Parameters ---------- + estimator: torch.nn.Sequential + PyTorch estimator for imputing a column based on the others. X : pd.DataFrame The input data for training. y : pd.DataFrame @@ -78,36 +89,41 @@ def _fit_estimator(self, estimator: nn.Sequential, X: pd.DataFrame, y: pd.DataFr ------- Any Return fitted PyTorch estimator. + """ if not estimator: raise EstimatorNotDefined() optimizer = optim.Adam(estimator.parameters(), lr=self.learning_rate) loss_fn = self.loss_fn - if estimator is None: - assert EstimatorNotDefined() - else: - for epoch in range(self.epochs): - estimator.train() - optimizer.zero_grad() - - input_data = torch.Tensor(X.values) - target_data = torch.Tensor(y.values) - target_data = target_data.unsqueeze(1) - outputs = estimator(input_data) - loss = loss_fn(outputs, target_data) - - loss.backward() - optimizer.step() - if (epoch + 1) % 10 == 0: - print(f"Epoch [{epoch + 1}/{self.epochs}], Loss: {loss.item():.4f}") + + for epoch in range(self.epochs): + estimator.train() + optimizer.zero_grad() + + input_data = torch.Tensor(X.values) + target_data = torch.Tensor(y.values) + target_data = target_data.unsqueeze(1) + outputs = estimator(input_data) + loss = loss_fn(outputs, target_data) + + loss.backward() + optimizer.step() + if (epoch + 1) % 10 == 0: + print( + f"Epoch [{epoch + 1}/{self.epochs}], " + f"Loss: {loss.item():.4f}" + ) return estimator - def _predict_estimator(self, estimator: nn.Sequential, X: pd.DataFrame) -> pd.Series: - """ - Perform predictions using the trained PyTorch estimator. + def _predict_estimator( + self, estimator: nn.Sequential, X: pd.DataFrame + ) -> pd.Series: + """Perform predictions using the trained PyTorch estimator. Parameters ---------- + estimator: torch.nn.Sequential + PyTorch estimator for imputing a column based on the others. X : pd.DataFrame The input data for prediction. @@ -120,6 +136,7 @@ def _predict_estimator(self, estimator: nn.Sequential, X: pd.DataFrame) -> pd.Se ------ EstimatorNotDefined Raises an error if the attribute estimator is not defined. + """ if not estimator: raise EstimatorNotDefined() @@ -130,8 +147,7 @@ def _predict_estimator(self, estimator: nn.Sequential, X: pd.DataFrame) -> pd.Se class Autoencoder(nn.Module): - """ - Wrapper of a PyTorch autoencoder allowing to encode + """Wrapper of a PyTorch autoencoder allowing to encode. Parameters ---------- @@ -145,6 +161,7 @@ class Autoencoder(nn.Module): Learning rate for optimization, by default 0.001. loss_fn : Callable, optional Loss function for training, by default nn.L1Loss(). + """ def __init__( @@ -166,8 +183,7 @@ def __init__( self.scaler = StandardScaler() def forward(self, x: NDArray) -> nn.Sequential: - """ - Forward pass through the autoencoder. + """Forward pass through the autoencoder. Parameters ---------- @@ -178,14 +194,14 @@ def forward(self, x: NDArray) -> nn.Sequential: ------- pd.DataFrame Decoded data. + """ encode = self.encoder(x) decode = self.decoder(encode) return decode - def fit(self, X: NDArray, y: NDArray) -> Self: - """ - Fit the autoencoder to the data. + def fit(self, X: NDArray, y: NDArray) -> "Autoencoder": + """Fit the autoencoder to the data. Parameters ---------- @@ -198,6 +214,7 @@ def fit(self, X: NDArray, y: NDArray) -> Self: ------- Self Return Self + """ optimizer = optim.Adam(self.parameters(), lr=self.learning_rate) loss_fn = self.loss_fn @@ -214,14 +231,16 @@ def fit(self, X: NDArray, y: NDArray) -> Self: loss.backward() optimizer.step() if (epoch + 1) % 10 == 0: - print(f"Epoch [{epoch + 1}/{self.epochs}], Loss: {loss.item():.4f}") + print( + f"Epoch [{epoch + 1}/{self.epochs}], " + f"Loss: {loss.item():.4f}" + ) list_loss.append(loss.item()) self.loss.extend([list_loss]) return self def decode(self, Z: NDArray) -> NDArray: - """ - Decode encoded data. + """Decode encoded data. Parameters ---------- @@ -232,6 +251,7 @@ def decode(self, Z: NDArray) -> NDArray: ------- ndarray Decoded data. + """ Z_decoded = self.scaler.inverse_transform(Z) Z_decoded = self.decoder(torch.Tensor(Z_decoded)) @@ -239,8 +259,7 @@ def decode(self, Z: NDArray) -> NDArray: return Z_decoded def encode(self, X: NDArray) -> NDArray: - """ - Encode input data. + """Encode input data. Parameters ---------- @@ -251,6 +270,7 @@ def encode(self, X: NDArray) -> NDArray: ------- ndarray Encoded data. + """ X_encoded = self.encoder(torch.Tensor(X)) X_encoded = X_encoded.detach().numpy() @@ -275,6 +295,7 @@ class ImputerAutoencoder(_Imputer): Learning rate hen fitting the autoencoder, by default 0.001 loss_fn: Callable Loss used when fitting the autoencoder, by default nn.L1Loss() + """ def __init__( @@ -289,7 +310,12 @@ def __init__( learning_rate: float = 0.001, loss_fn: Callable = nn.L1Loss(), ) -> None: - super().__init__(groups=groups, columnwise=False, shrink=False, random_state=random_state) + super().__init__( + groups=groups, + columnwise=False, + shrink=False, + random_state=random_state, + ) self.loss_fn = loss_fn self.lamb = lamb self.max_iterations = max_iterations @@ -298,10 +324,13 @@ def __init__( self.encoder = encoder self.decoder = decoder - def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) -> Autoencoder: - """ - Fits the imputer on `df`, at the group and/or column level depending onself.groups and - self.columnwise. + def _fit_element( + self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 + ) -> Autoencoder: + """Fit the imputer on `df`. + + It does that at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -321,6 +350,7 @@ def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) autoencoder = Autoencoder( @@ -336,9 +366,10 @@ def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0) def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending onself.groups and - self.columnwise. + """Transform the dataframe `df`. + + It does that at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -358,6 +389,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ autoencoder = self._dict_fitting[col][ngroup] df_train = df.copy() @@ -378,7 +410,9 @@ def _transform_element( X_next = autoencoder.decode(Z_next) X[mask] = X_next[mask] df_imputed = pd.DataFrame( - scaler.inverse_transform(X), index=df_train.index, columns=df_train.columns + scaler.inverse_transform(X), + index=df_train.index, + columns=df_train.columns, ) return df_imputed @@ -389,8 +423,7 @@ def build_mlp( output_dim: int = 1, activation: Callable = nn.ReLU, ) -> nn.Sequential: - """ - Constructs a multi-layer perceptron (MLP) with a custom architecture. + """Construct a multi-layer perceptron (MLP) with a custom architecture. Parameters ---------- @@ -401,7 +434,8 @@ def build_mlp( output_dim : int, optional Dimension of the output layer, defaults to 1. activation : nn.Module, optional - Activation function to use between hidden layers, defaults to nn.ReLU(). + Activation function to use between hidden layers, + defaults to nn.ReLU(). Returns ------- @@ -415,7 +449,9 @@ def build_mlp( Examples -------- - >>> model = build_mlp(input_dim=10, list_num_neurons=[32, 64, 128], output_dim=1) + >>> model = build_mlp( + ... input_dim=10, list_num_neurons=[32, 64, 128], output_dim=1 + ... ) >>> print(model) Sequential( (0): Linear(in_features=10, out_features=32, bias=True) @@ -426,6 +462,7 @@ def build_mlp( (5): ReLU() (6): Linear(in_features=128, out_features=1, bias=True) ) + """ layers = [] for num_neurons in list_num_neurons: @@ -445,8 +482,7 @@ def build_autoencoder( output_dim: int = 1, activation: Callable = nn.ReLU, ) -> Tuple[nn.Sequential, nn.Sequential]: - """ - Constructs an autoencoder with a custom architecture. + """Construct an autoencoder with a custom architecture. Parameters ---------- @@ -459,7 +495,8 @@ def build_autoencoder( output_dim : int, optional Dimension of the output layer, defaults to 1. activation : nn.Module, optional - Activation function to use between hidden layers, defaults to nn.ReLU(). + Activation function to use between hidden layers, + defaults to nn.ReLU(). Returns ------- @@ -473,10 +510,12 @@ def build_autoencoder( Examples -------- - >>> encoder, decoder = build_autoencoder(input_dim=10, - ... latent_dim=4, - ... list_num_neurons=[32, 64, 128], - ... output_dim=252) + >>> encoder, decoder = build_autoencoder( + ... input_dim=10, + ... latent_dim=4, + ... list_num_neurons=[32, 64, 128], + ... output_dim=252, + ... ) >>> print(encoder) Sequential( (0): Linear(in_features=10, out_features=128, bias=True) @@ -497,8 +536,8 @@ def build_autoencoder( (5): ReLU() (6): Linear(in_features=128, out_features=252, bias=True) ) - """ + """ encoder = build_mlp( input_dim=input_dim, output_dim=latent_dim, @@ -515,7 +554,9 @@ def build_autoencoder( class ImputerDiffusion(_Imputer): - """This class inherits from the class _Imputer. + """Imputer based on diffusion models. + + This class inherits from the class _Imputer. It is a wrapper for imputers based on diffusion models. """ @@ -536,8 +577,7 @@ def __init__( index_datetime: str = "", freq_str: str = "1D", ): - """This class inherits from the class _Imputer. - It is a wrapper for imputers based on diffusion models. + """Init ImputerDiffusion. Parameters ---------- @@ -555,8 +595,8 @@ def __init__( print_valid : bool, optional Print model performance for after several epochs, by default False metrics_valid : Tuple[Callable, ...], optional - Set of validation metrics, by default ( metrics.mean_absolute_error, - metrics.dist_wasserstein ) + Set of validation metrics, by default (metrics.mean_absolute_error, + metrics.dist_wasserstein) round : int, optional Number of decimal places to round to, for better displaying model performance, by default 10 @@ -564,10 +604,12 @@ def __init__( Name of columns that need to be imputed, by default () index_datetime : str Name of datetime-like index. - It is for processing time-series data, used in diffusion models e.g., TsDDPM. + It is for processing time-series data, used in diffusion models + e.g., TsDDPM. freq_str : str Frequency string of DateOffset of Pandas. - It is for processing time-series data, used in diffusion models e.g., TsDDPM. + It is for processing time-series data, used in diffusion models + e.g., TsDDPM. Examples -------- @@ -575,10 +617,20 @@ def __init__( >>> from qolmat.imputations.imputers_pytorch import ImputerDiffusion >>> from qolmat.imputations.diffusions.ddpms import TabDDPM >>> - >>> X = np.array([[1, 1, 1, 1], [np.nan, np.nan, 3, 2], [1, 2, 2, 1], [2, 2, 2, 2]]) - >>> imputer = ImputerDiffusion(model=TabDDPM(random_state=11), epochs=50, batch_size=1) + >>> X = np.array( + ... [ + ... [1, 1, 1, 1], + ... [np.nan, np.nan, 3, 2], + ... [1, 2, 2, 1], + ... [2, 2, 2, 2], + ... ] + ... ) + >>> imputer = ImputerDiffusion( + ... model=TabDDPM(random_state=11), epochs=50, batch_size=1 + ... ) >>> >>> df_imputed = imputer.fit_transform(X) + """ super().__init__(groups=groups, columnwise=False) self.model = model @@ -603,10 +655,13 @@ def _more_tags(self): }, } - def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0): - """ - Fits the imputer on `df`, at the group and/or column level depending onself.groups and - self.columnwise. + def _fit_element( + self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 + ): + """Fit the imputer on `df`. + + It does it at the group and/or column level depending onself.groups + and self.columnwise. Parameters ---------- @@ -626,6 +681,7 @@ def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0): ------ NotDataFrame Input has to be a pandas.DataFrame. + """ self._check_dataframe(df) hp = self._get_params_fit() @@ -634,8 +690,9 @@ def _fit_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0): def _transform_element( self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 ) -> pd.DataFrame: - """ - Transforms the dataframe `df`, at the group and/or column level depending on self.groups + """Transform the dataframe `df`. + + It does it at the group and/or column level depending on self.groups and self.columnwise. Parameters @@ -656,6 +713,7 @@ def _transform_element( ------ NotDataFrame Input has to be a pandas.DataFrame. + """ df_imputed = self.model.predict(df) return df_imputed @@ -682,9 +740,25 @@ def _get_params_fit(self) -> Dict: return hyperparams def get_summary_training(self) -> Dict: + """Get the summary of the training. + + Returns + ------- + Dict + Summary of the training + + """ return self.model.summary def get_summary_architecture(self) -> Dict: + """Get the summary of the architecture. + + Returns + ------- + Dict + Summary of the architecture + + """ return { "number_parameters": self.model.num_params, "epsilon_model": self.model._eps_model, diff --git a/qolmat/imputations/preprocessing.py b/qolmat/imputations/preprocessing.py index 50c54270..12308ffb 100644 --- a/qolmat/imputations/preprocessing.py +++ b/qolmat/imputations/preprocessing.py @@ -1,38 +1,39 @@ +"""Script for preprocessing functions.""" + import copy -from typing import Any, Dict, Hashable, List, Optional, Tuple +from typing import Dict, Hashable, List, Optional, Tuple + import numpy as np import pandas as pd -from sklearn.compose import make_column_selector as selector -from sklearn.preprocessing import StandardScaler -from sklearn.pipeline import Pipeline -from sklearn.ensemble import ( - HistGradientBoostingRegressor, - HistGradientBoostingClassifier, -) -from sklearn.compose import ColumnTransformer +from category_encoders.one_hot import OneHotEncoder +from numpy.typing import NDArray from sklearn.base import ( BaseEstimator, RegressorMixin, TransformerMixin, ) +from sklearn.compose import ColumnTransformer +from sklearn.compose import make_column_selector as selector +from sklearn.ensemble import ( + HistGradientBoostingClassifier, + HistGradientBoostingRegressor, +) +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import StandardScaler from sklearn.utils.validation import ( - check_X_y, check_array, check_is_fitted, + check_X_y, ) -from category_encoders.one_hot import OneHotEncoder - - -from typing_extensions import Self -from numpy.typing import NDArray - +# from typing_extensions import Self from qolmat.utils import utils class MixteHGBM(RegressorMixin, BaseEstimator): - """ - A custom scikit-learn estimator implementing a mixed model using + """MixteHGBM class. + + This is a custom scikit-learn estimator implementing a mixed model using HistGradientBoostingClassifier for string target data and HistGradientBoostingRegressor for numeric target data. """ @@ -41,19 +42,18 @@ def __init__(self): super().__init__() def set_model_parameters(self, **args_model): - """ - Sets the arguments of the underlying model. + """Set the arguments of the underlying model. Parameters ---------- - **kwargs : dict + **args_model : dict Additional keyword arguments to be passed to the underlying models. + """ self.args_model = args_model - def fit(self, X: NDArray, y: NDArray) -> Self: - """ - Fit the model according to the given training data. + def fit(self, X: NDArray, y: NDArray) -> "MixteHGBM": + """Fit the model according to the given training data. Parameters ---------- @@ -66,8 +66,11 @@ def fit(self, X: NDArray, y: NDArray) -> Self: ------- self : object Returns self. + """ - X, y = check_X_y(X, y, accept_sparse=True, force_all_finite="allow-nan") + X, y = check_X_y( + X, y, accept_sparse=True, force_all_finite="allow-nan" + ) self.is_fitted_ = True self.n_features_in_ = X.shape[1] if hasattr(self, "args_model"): @@ -85,8 +88,7 @@ def fit(self, X: NDArray, y: NDArray) -> Self: return self def predict(self, X: NDArray) -> NDArray: - """ - Predict using the fitted model. + """Predict using the fitted model. Parameters ---------- @@ -97,6 +99,7 @@ def predict(self, X: NDArray) -> NDArray: ------- y_pred : array-like, shape (n_samples,) Predicted target values. + """ X = check_array(X, accept_sparse=True, force_all_finite="allow-nan") check_is_fitted(self, "is_fitted_") @@ -104,26 +107,29 @@ def predict(self, X: NDArray) -> NDArray: return y_pred def _more_tags(self): + """Indicate if the class allows inputs with categorical data and nans. + + It modifies the behaviour of the functions checking data. """ - This method indicates that this class allows inputs with categorical data and nans. It - modifies the behaviour of the functions checking data. - """ - return {"X_types": ["2darray", "categorical", "string"], "allow_nan": True} + return { + "X_types": ["2darray", "categorical", "string"], + "allow_nan": True, + } class BinTransformer(TransformerMixin, BaseEstimator): - """ - Learns the possible values of the provided numerical feature, allowing to transform new values - to the closest existing one. + """BinTransformer class. + + Learn the possible values of the provided numerical feature, + allowing to transform new values to the closest existing one. """ def __init__(self, cols: Optional[List] = None): super().__init__() self.cols = cols - def fit(self, X: NDArray, y: Optional[NDArray] = None) -> Self: - """ - Fit the BinTransformer to X. + def fit(self, X: NDArray, y: Optional[NDArray] = None) -> "BinTransformer": + """Fit the BinTransformer to X. Parameters ---------- @@ -138,11 +144,12 @@ def fit(self, X: NDArray, y: Optional[NDArray] = None) -> Self: ------- self : object Fitted transformer. + """ df = utils._validate_input(X) self.feature_names_in_ = df.columns self.n_features_in_ = len(df.columns) - self.dict_df_bins_: Dict[Hashable, pd.DataFrame] = dict() + self.dict_df_bins_: Dict[Hashable, pd.DataFrame] = {} if self.cols is None: cols = df.select_dtypes(include="number").columns else: @@ -156,8 +163,7 @@ def fit(self, X: NDArray, y: Optional[NDArray] = None) -> Self: return self def transform(self, X: NDArray) -> NDArray: - """ - Transform X to existing values learned during fit. + """Transform X to existing values learned during fit. Parameters ---------- @@ -168,6 +174,7 @@ def transform(self, X: NDArray) -> NDArray: ------- X_out : ndarray of shape (n_samples,) Transformed input. + """ df = utils._validate_input(X) check_is_fitted(self) @@ -176,7 +183,8 @@ def transform(self, X: NDArray) -> NDArray: or df.columns.to_list() != self.feature_names_in_.to_list() ): raise ValueError( - "Feature names in X {df.columns} don't match with expected {feature_names_in_}" + f"Feature names in X {df.columns} don't match with " + f"expected {self.feature_names_in_}" ) df_out = df.copy() for col in df: @@ -192,8 +200,7 @@ def transform(self, X: NDArray) -> NDArray: return df_out def inverse_transform(self, X: NDArray) -> NDArray: - """ - Transform X to existing values learned during fit. + """Transform X to existing values learned during fit. Parameters ---------- @@ -204,37 +211,43 @@ def inverse_transform(self, X: NDArray) -> NDArray: ------- X_out : ndarray of shape (n_samples,) Transformed input. + """ return self.transform(X) def _more_tags(self): + """Indicate if the class allows inputs with categorical data and nans. + + It modifies the behaviour of the functions checking data. """ - This method indicates that this class allows inputs with categorical data and nans. It - modifies the behaviour of the functions checking data. - """ - return {"X_types": ["2darray", "categorical", "string"], "allow_nan": True} + return { + "X_types": ["2darray", "categorical", "string"], + "allow_nan": True, + } class OneHotEncoderProjector(OneHotEncoder): - """ - Inherits from the class OneHotEncoder imported from category_encoders. The decoding - function accepts non boolean values (as it is the case for the sklearn OneHotEncoder). In - this case the decoded value corresponds to the largest dummy value. + """Class for one-hot encoding of categorical features. + + It inherits from the class OneHotEncoder imported from category_encoders. + The decoding function accepts non boolean values (as it is the case for + the sklearn OneHotEncoder). In this case the decoded value corresponds to + the largest dummy value. """ def __init__(self, **kwargs): super().__init__(**kwargs) def reverse_dummies(self, X: pd.DataFrame, mapping: Dict) -> pd.DataFrame: - """ - Convert dummy variable into numerical variables + """Convert dummy variable into numerical variables. Parameters ---------- X : DataFrame + Input dataframe. mapping: list-like - Contains mappings of column to be transformed to it's new columns and value - represented + Mapping of column to be transformed to its + new columns and value represented Returns ------- @@ -260,22 +273,55 @@ def reverse_dummies(self, X: pd.DataFrame, mapping: Dict) -> pd.DataFrame: class WrapperTransformer(TransformerMixin, BaseEstimator): - """ - Wraps a transformer with reversible transformers designed to embed the data. + """Wrap a transformer. + + Wrapper with reversible transformers designed to embed the data. """ - def __init__(self, transformer: TransformerMixin, wrapper: TransformerMixin): + def __init__( + self, transformer: TransformerMixin, wrapper: TransformerMixin + ): super().__init__() self.transformer = transformer self.wrapper = wrapper - def fit(self, X: NDArray, y: Optional[NDArray] = None) -> Self: + def fit( + self, X: NDArray, y: Optional[NDArray] = None + ) -> "WrapperTransformer": + """Fit the model according to the given training data. + + Parameters + ---------- + X : NDArray + Input array. + y : Optional[NDArray], optional + _description_, by default None + + Returns + ------- + Self + The object itself. + + """ X_transformed = copy.deepcopy(X) X_transformed = self.wrapper.fit_transform(X_transformed) X_transformed = self.transformer.fit(X_transformed) return self def fit_transform(self, X: NDArray) -> NDArray: + """Fit the model according to the given training data and transform it. + + Parameters + ---------- + X : NDArray + Input array. + + Returns + ------- + NDArray + Transformed array. + + """ X_transformed = copy.deepcopy(X) X_transformed = self.wrapper.fit_transform(X_transformed) X_transformed = self.transformer.fit_transform(X_transformed) @@ -283,6 +329,19 @@ def fit_transform(self, X: NDArray) -> NDArray: return X_transformed def transform(self, X: NDArray) -> NDArray: + """Transform X. + + Parameters + ---------- + X : NDArray + Input array. + + Returns + ------- + NDArray + Transformed array. + + """ X_transformed = copy.deepcopy(X) X_transformed = self.wrapper.transform(X_transformed) X_transformed = self.transformer.transform(X_transformed) @@ -293,8 +352,9 @@ def transform(self, X: NDArray) -> NDArray: def make_pipeline_mixte_preprocessing( scale_numerical: bool = False, avoid_new: bool = False ) -> Pipeline: - """ - Create a preprocessing pipeline managing mixed type data by one hot encoding categorical data. + """Create a preprocessing pipeline managing mixed type data. + + It does this by one hot encoding categorical data. Parameters ---------- @@ -307,14 +367,19 @@ def make_pipeline_mixte_preprocessing( ------- preprocessor : Pipeline Preprocessing pipeline + """ transformers: List[Tuple] = [] if scale_numerical: - transformers += [("num", StandardScaler(), selector(dtype_include=np.number))] + transformers += [ + ("num", StandardScaler(), selector(dtype_include=np.number)) + ] ohe = OneHotEncoder(handle_unknown="ignore", use_cat_names=True) transformers += [("cat", ohe, selector(dtype_exclude=np.number))] - col_transformer = ColumnTransformer(transformers=transformers, remainder="passthrough") + col_transformer = ColumnTransformer( + transformers=transformers, remainder="passthrough" + ) col_transformer = col_transformer.set_output(transform="pandas") preprocessor = Pipeline(steps=[("col_transformer", col_transformer)]) @@ -323,13 +388,19 @@ def make_pipeline_mixte_preprocessing( return preprocessor -def make_robust_MixteHGB(scale_numerical: bool = False, avoid_new: bool = False) -> Pipeline: - """ - Create a robust pipeline for MixteHGBM by one hot encoding categorical features. - This estimator is intended for use in ImputerRegressor to deal with mixed type data. +def make_robust_MixteHGB( + scale_numerical: bool = False, avoid_new: bool = False +) -> Pipeline: + """Create a robust pipeline for MixteHGBM. - Note that from sklearn 1.4 HistGradientBoosting Natively Supports Categorical DTypes in - DataFrames, so that this pipeline is not required anymore. + Create a preprocessing pipeline managing mixed type data + by one hot encoding categorical features. + This estimator is intended for use in ImputerRegressor + to deal with mixed type data. + + Note that from sklearn 1.4 HistGradientBoosting Natively Supports + Categorical DTypes in DataFrames, so that this pipeline is not + required anymore. Parameters @@ -343,6 +414,7 @@ def make_robust_MixteHGB(scale_numerical: bool = False, avoid_new: bool = False) ------- robust_MixteHGB : object A robust pipeline for MixteHGBM. + """ preprocessor = make_pipeline_mixte_preprocessing( scale_numerical=scale_numerical, avoid_new=avoid_new diff --git a/qolmat/imputations/rpca/rpca.py b/qolmat/imputations/rpca/rpca.py index 29eeaaf9..a081eae3 100644 --- a/qolmat/imputations/rpca/rpca.py +++ b/qolmat/imputations/rpca/rpca.py @@ -1,18 +1,15 @@ +"""Script for the root class of RPCA.""" + from __future__ import annotations -from typing import Union, Tuple -from typing_extensions import Self +from typing import Union import numpy as np -from numpy.typing import NDArray from sklearn.base import BaseEstimator, TransformerMixin -from qolmat.utils import utils - class RPCA(BaseEstimator, TransformerMixin): - """ - This class is the root class of the RPCA methods. + """Root class of the RPCA methods. Parameters ---------- @@ -24,6 +21,7 @@ class RPCA(BaseEstimator, TransformerMixin): Tolerance for stopping criteria, by default 1e-6 verbose: bool default `False` + """ def __init__( diff --git a/qolmat/imputations/rpca/rpca_noisy.py b/qolmat/imputations/rpca/rpca_noisy.py index 74e68856..ae59ae0a 100644 --- a/qolmat/imputations/rpca/rpca_noisy.py +++ b/qolmat/imputations/rpca/rpca_noisy.py @@ -1,13 +1,15 @@ +"""Script for an the noisy RPCA.""" + from __future__ import annotations import warnings -from typing import Dict, List, Optional, Tuple, TypeVar, Union +from typing import Dict, List, Optional, Tuple, Union import numpy as np import scipy as scp +from numpy.typing import NDArray from scipy.sparse import dok_matrix, identity from scipy.sparse.linalg import spsolve -from numpy.typing import NDArray from sklearn import utils as sku from qolmat.imputations.rpca import rpca_utils @@ -16,23 +18,23 @@ class RpcaNoisy(RPCA): - """ - This class implements a noisy version of the so-called 'improved RPCA' + """Clas for a noisy version of the so-called 'improved RPCA'. References ---------- - Wang, Xuehui, et al. "An improved robust principal component analysis model for anomalies - detection of subway passenger flow." + Wang, Xuehui, et al. "An improved robust principal component analysis model + for anomalies detection of subway passenger flow." Journal of advanced transportation (2018). - Chen, Yuxin, et al. "Bridging convex and nonconvex optimization in robust PCA: Noise, outliers - and missing data." + Chen, Yuxin, et al. "Bridging convex and nonconvex optimization + in robust PCA: Noise, outliers and missing data." The Annals of Statistics 49.5 (2021): 2948-2971. Parameters ---------- random_state : int, optional - The seed of the pseudo random number generator to use, for reproductibility. + The seed of the pseudo random number generator to use, + for reproductibility. rank: Optional[int] Upper bound of the rank to be estimated mu: Optional[float] @@ -44,16 +46,19 @@ class RpcaNoisy(RPCA): list_periods: Optional[List[int]] list of periods, linked to the Toeplitz matrices list_etas: Optional[List[float]] - list of penalizing parameters for the corresponding period in list_periods + list of penalizing parameters for the corresponding period + in list_periods max_iterations: Optional[int] - stopping criteria, maximum number of iterations. By default, the value is set to 10_000 + stopping criteria, maximum number of iterations. + By default, the value is set to 10_000 tolerance: Optional[float] - stoppign critera, minimum difference between 2 consecutive iterations. By default, - the value is set to 1e-6 + stoppign critera, minimum difference between 2 consecutive iterations. + By default, the value is set to 1e-6 norm: Optional[str] error norm, can be "L1" or "L2". By default, the value is set to "L2" verbose: Optional[bool] verbosity level, if False the warnings are silenced + """ def __init__( @@ -70,7 +75,9 @@ def __init__( norm: str = "L2", verbose: bool = True, ) -> None: - super().__init__(max_iterations=max_iterations, tolerance=tolerance, verbose=verbose) + super().__init__( + max_iterations=max_iterations, tolerance=tolerance, verbose=verbose + ) self.rng = sku.check_random_state(random_state) self.rank = rank self.mu = mu @@ -81,8 +88,7 @@ def __init__( self.norm = norm def get_params_scale(self, D: NDArray) -> Dict[str, float]: - """ - Get parameters for scaling in RPCA based on the input data. + """Get parameters for scaling in RPCA based on the input data. Parameters ---------- @@ -111,8 +117,7 @@ def get_params_scale(self, D: NDArray) -> Dict[str, float]: } def decompose(self, D: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: - """ - Compute the noisy RPCA with L1 or L2 time penalisation + """Compute the noisy RPCA with L1 or L2 time penalisation. Parameters ---------- @@ -127,6 +132,7 @@ def decompose(self, D: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: Low-rank signal A: NDArray Anomalies + """ M, A, _, _ = self.decompose_with_basis(D, Omega) return M, A @@ -134,9 +140,9 @@ def decompose(self, D: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: def decompose_with_basis( self, D: NDArray, Omega: NDArray ) -> Tuple[NDArray, NDArray, NDArray, NDArray]: - """ - Compute the noisy RPCA with L1 or L2 time penalisation, and returns the decomposition of - the low-rank matrix. + """Compute the noisy RPCA with L1 or L2 time penalisation. + + It returns the decomposition of the low-rank matrix. Parameters ---------- @@ -155,6 +161,7 @@ def decompose_with_basis( Coefficients of the low-rank matrix in the reduced basis Q: NDArray Reduced basis of the low-rank matrix + """ D = utils.linear_interpolation(D) self.params_scale = self.get_params_scale(D) @@ -175,8 +182,9 @@ def decompose_with_basis( for period in self.list_periods: if not period < n_rows: raise ValueError( - "The periods provided in argument in `list_periods` must smaller " - f"than the number of rows in the matrix but {period} >= {n_rows}!" + "The periods provided in argument in `list_periods` " + "must smaller than the number of rows " + f"in the matrix but {period} >= {n_rows}!" ) M, A, L, Q = self.minimise_loss( @@ -211,12 +219,12 @@ def minimise_loss( tolerance: float = 1e-6, norm: str = "L2", ) -> Tuple: - """ - Compute the noisy RPCA with a L2 time penalisation. + """Compute the noisy RPCA with a L2 time penalisation. - This function computes the noisy Robust Principal Component Analysis (RPCA) using a L2 time - penalisation. It iteratively minimizes a loss function to separate the low-rank and sparse - components from the input data matrix. + This function computes the noisy Robust Principal Component Analysis + (RPCA) using a L2 time penalisation. It iteratively minimizes a loss + function to separate the low-rank and sparse components from the + input data matrix. Parameters ---------- @@ -231,18 +239,19 @@ def minimise_loss( lam : float Penalizing parameter for the sparse matrix. mu : float, optional - Initial stiffness parameter for the constraint on M, L, and Q. Defaults - to 1e-2. + Initial stiffness parameter for the constraint on M, L, and Q. + Defaults to 1e-2. list_periods : List[int], optional List of periods linked to the Toeplitz matrices. Defaults to []. list_etas : List[float], optional - List of penalizing parameters for the corresponding periods in list_periods. Defaults + List of penalizing parameters for the corresponding periods + in list_periods. Defaults to []. max_iterations : int, optional Stopping criteria, maximum number of iterations. Defaults to 10000. tolerance : float, optional - Stopping criteria, minimum difference between 2 consecutive iterations. - Defaults to 1e-6. + Stopping criteria, minimum difference between 2 + consecutive iterations. Defaults to 1e-6. norm : str, optional Error norm, can be "L1" or "L2". Defaults to "L2". @@ -264,8 +273,8 @@ def minimise_loss( ValueError If the periods provided in the argument in `list_periods` are not smaller than the number of rows in the matrix. - """ + """ rho = 1.1 n_rows, n_cols = D.shape @@ -288,10 +297,15 @@ def minimise_loss( mu_bar = mu * 1e3 # matrices for temporal correlation - list_H = [rpca_utils.toeplitz_matrix(period, n_rows) for period in list_periods] + list_H = [ + rpca_utils.toeplitz_matrix(period, n_rows) + for period in list_periods + ] HtH = dok_matrix((n_rows, n_rows)) for i_period, _ in enumerate(list_periods): - HtH += list_etas[i_period] * (list_H[i_period].T @ list_H[i_period]) + HtH += list_etas[i_period] * ( + list_H[i_period].T @ list_H[i_period] + ) Ir = np.eye(rank) In = identity(n_rows) @@ -335,7 +349,9 @@ def minimise_loss( if norm == "L1": for i_period, _ in enumerate(list_periods): eta = list_etas[i_period] - R[i_period] = rpca_utils.soft_thresholding(R[i_period] / mu, eta / mu) + R[i_period] = rpca_utils.soft_thresholding( + R[i_period] / mu, eta / mu + ) mu = min(mu * rho, mu_bar) @@ -364,9 +380,11 @@ def decompose_on_basis( Omega: NDArray, Q: NDArray, ) -> Tuple[NDArray, NDArray]: - """ - Decompose the matrix D with an observation matrix Omega using the noisy RPCA algorithm, - with a fixed reduced basis given by the matrix Q. This allows to impute new data without + """Decompose the matrix D with an observation matrix Omega. + + It uses the noisy RPCA algorithm, + with a fixed reduced basis given by the matrix Q. + This allows to impute new data without resolving the optimization problem on the whole dataset. Parameters @@ -384,6 +402,7 @@ def decompose_on_basis( A tuple representing the decomposition of D with: - M: low-rank matrix - A: sparse matrix + """ D = utils.linear_interpolation(D) params_scale = self.get_params_scale(D) @@ -434,8 +453,9 @@ def _check_cost_function_minimized( tau: float, lam: float, ): - """ - Check that the functional minimized by the RPCA is smaller at the end than at the + """Check cost function. + + The functional minimized by the RPCA is smaller at the end than at the beginning. Parameters @@ -452,6 +472,7 @@ def _check_cost_function_minimized( parameter penalizing the nuclear norm of the low rank part lam : float parameter penalizing the L1-norm of the anomaly/sparse part + """ cost_start = self.cost_function( D, @@ -482,8 +503,11 @@ def _check_cost_function_minimized( if self.verbose and (cost_end > cost_start * (1 + 1e-6)): warnings.warn( - f"RPCA algorithm may provide bad results. Function {function_str} increased from" - f" {cost_start} to {cost_end} instead of decreasing!".format("%.2f") + "RPCA algorithm may provide bad results. " + f"Function {function_str} increased from" + f" {cost_start} to {cost_end} instead of decreasing!".format( + "%.2f" + ) ) @staticmethod @@ -498,8 +522,7 @@ def cost_function( list_etas: List[float] = [], norm: str = "L2", ): - """ - Estimated cost function for the noisy RPCA algorithm + """Estimate cost function for the noisy RPCA algorithm. Parameters ---------- @@ -518,27 +541,34 @@ def cost_function( list_periods: Optional[List[int]] list of periods, linked to the Toeplitz matrices list_etas: Optional[List[float]] - list of penalizing parameters for the corresponding period in list_periods + list of penalizing parameters for the corresponding period in + list_periods norm: Optional[str] - error norm, can be "L1" or "L2". By default, the value is set to "L2" + error norm, can be "L1" or "L2". + By default, the value is set to "L2" Returns ------- float Value of the cost function minimized by the RPCA - """ + """ temporal_norm: float = 0 if len(list_etas) > 0: # matrices for temporal correlation - list_H = [rpca_utils.toeplitz_matrix(period, D.shape[0]) for period in list_periods] + list_H = [ + rpca_utils.toeplitz_matrix(period, D.shape[0]) + for period in list_periods + ] if norm == "L1": for eta, H_matrix in zip(list_etas, list_H): temporal_norm += eta * np.sum(np.abs(H_matrix @ M)) elif norm == "L2": for eta, H_matrix in zip(list_etas, list_H): - temporal_norm += eta * float(np.linalg.norm(H_matrix @ M, "fro")) + temporal_norm += eta * float( + np.linalg.norm(H_matrix @ M, "fro") + ) anomalies_norm = np.sum(np.abs(A * Omega)) cost = ( 1 / 2 * ((Omega * (D - M - A)) ** 2).sum() diff --git a/qolmat/imputations/rpca/rpca_pcp.py b/qolmat/imputations/rpca/rpca_pcp.py index f3b8e751..500605fb 100644 --- a/qolmat/imputations/rpca/rpca_pcp.py +++ b/qolmat/imputations/rpca/rpca_pcp.py @@ -1,3 +1,5 @@ +"""Script for the PCP RPCA.""" + from __future__ import annotations import warnings @@ -13,8 +15,9 @@ class RpcaPcp(RPCA): - """ - This class implements the basic RPCA decomposition using Alternating Lagrangian Multipliers. + """Class for the basic RPCA decomposition. + + It uses Alternating Lagrangian Multipliers. References ---------- @@ -24,7 +27,8 @@ class RpcaPcp(RPCA): Parameters ---------- random_state : int, optional - The seed of the pseudo random number generator to use, for reproductibility. + The seed of the pseudo random number generator to use, + for reproductibility. period: Optional[int] number of rows of the reshaped matrix if the signal is a 1D-array rank: Optional[int] @@ -34,12 +38,14 @@ class RpcaPcp(RPCA): lam: Optional[float] penalizing parameter for the sparse matrix max_iterations: Optional[int] - stopping criteria, maximum number of iterations. By default, the value is set to 10_000 + stopping criteria, maximum number of iterations. + By default, the value is set to 10_000 tolerance: Optional[float] - stoppign critera, minimum difference between 2 consecutive iterations. By default, - the value is set to 1e-6 + stoppign critera, minimum difference between 2 consecutive iterations. + By default, the value is set to 1e-6 verbose: Optional[bool] verbosity level, if False the warnings are silenced + """ def __init__( @@ -51,14 +57,15 @@ def __init__( tolerance: float = 1e-6, verbose: bool = True, ) -> None: - super().__init__(max_iterations=max_iterations, tolerance=tolerance, verbose=verbose) + super().__init__( + max_iterations=max_iterations, tolerance=tolerance, verbose=verbose + ) self.rng = sku.check_random_state(random_state) self.mu = mu self.lam = lam def get_params_scale(self, D: NDArray): - """ - Get parameters for scaling in RPCA based on the input data. + """Get parameters for scaling in RPCA based on the input data. Parameters ---------- @@ -81,8 +88,9 @@ def get_params_scale(self, D: NDArray): return dict_params def decompose(self, D: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: - """ - Estimate the relevant parameters then compute the PCP RPCA decomposition, using the + """Estimate the relevant parameters. + + It computes the PCP RPCA decomposition, using the Augumented Largrangian Multiplier (ALM) Parameters @@ -98,6 +106,7 @@ def decompose(self, D: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: Low-rank signal A: NDArray Anomalies + """ D = utils.linear_interpolation(D) if np.all(D == 0): @@ -116,7 +125,6 @@ def decompose(self, D: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: M: NDArray = D - A for iteration in range(self.max_iterations): - M = rpca_utils.svd_thresholding(D - A + Y / mu, 1 / mu) A = rpca_utils.soft_thresholding(D - M + Y / mu, lam / mu) A[~Omega] = (D - M)[~Omega] @@ -141,7 +149,9 @@ def _check_cost_function_minimized( Omega: NDArray, lam: float, ): - """Check that the functional minimized by the RPCA + """Check that the functional minimized by the RPCA. + + Check that the functional minimized by the RPCA is smaller at the end than at the beginning Parameters @@ -156,12 +166,16 @@ def _check_cost_function_minimized( boolean matrix indicating the observed values lam : float parameter penalizing the L1-norm of the anomaly/sparse part + """ cost_start = np.linalg.norm(observations, "nuc") - cost_end = np.linalg.norm(low_rank, "nuc") + lam * np.sum(Omega * np.abs(anomalies)) + cost_end = np.linalg.norm(low_rank, "nuc") + lam * np.sum( + Omega * np.abs(anomalies) + ) if self.verbose and round(cost_start, 4) - round(cost_end, 4) <= -1e-2: function_str = "||D||_* + lam ||A||_1" warnings.warn( - f"RPCA algorithm may provide bad results. Function {function_str} increased from" - f" {cost_start} to {cost_end} instead of decreasing!" + "RPCA algorithm may provide bad results. " + f"Function {function_str} increased from {cost_start} " + f"to {cost_end} instead of decreasing!" ) diff --git a/qolmat/imputations/rpca/rpca_utils.py b/qolmat/imputations/rpca/rpca_utils.py index 9e6c8945..0d3b6d5f 100644 --- a/qolmat/imputations/rpca/rpca_utils.py +++ b/qolmat/imputations/rpca/rpca_utils.py @@ -1,12 +1,7 @@ -""" -Modular utility functions for RPCA -""" +"""Modular utility functions for RPCA.""" -from typing import Tuple import numpy as np from numpy.typing import NDArray -import scipy -from scipy.linalg import toeplitz from scipy import sparse as sps @@ -14,8 +9,7 @@ def approx_rank( M: NDArray, threshold: float = 0.95, ) -> int: - """ - Estimate a bound on the rank of an array by SVD. + """Estimate a bound on the rank of an array by SVD. Parameters ---------- @@ -45,8 +39,7 @@ def soft_thresholding( X: NDArray, threshold: float, ) -> NDArray: - """ - Shrinkage operator (i.e. soft thresholding) on the elements of X. + """Shrinkage operator (i.e. soft thresholding) on the elements of X. Parameters ---------- @@ -59,13 +52,13 @@ def soft_thresholding( ------- NDArray Array V such that V = sign(X) * max(abs(X - threshold,0) + """ return np.sign(X) * np.maximum(np.abs(X) - threshold, 0) def svd_thresholding(X: NDArray, threshold: float) -> NDArray: - """ - Apply the shrinkage operator to the singular values obtained from the SVD of X. + """Apply shrinkage to the singular values from X's SVD. Parameters ---------- @@ -81,6 +74,7 @@ def svd_thresholding(X: NDArray, threshold: float) -> NDArray: U is the array of left singular vectors of X V is the array of the right singular vectors of X s is the array of the singular values as a diagonal array + """ U, s, Vh = np.linalg.svd(X, full_matrices=False) s = soft_thresholding(s, threshold) @@ -88,8 +82,7 @@ def svd_thresholding(X: NDArray, threshold: float) -> NDArray: def l1_norm(M: NDArray) -> float: - """ - L1 norm of an array + """Compute the L1 norm of an array. Parameters ---------- @@ -100,13 +93,15 @@ def l1_norm(M: NDArray) -> float: ------- float L1 norm of M + """ return np.sum(np.abs(M)) -def toeplitz_matrix(T: int, dimension: int) -> NDArray: - """ - Create a sparse Toeplitz square matrix H to take into account temporal correlations in the RPCA +def toeplitz_matrix(T: int, dimension: int) -> sps.spmatrix: + """Create a sparse Toeplitz square matrix H. + + It is useful to take into account temporal correlations in the RPCA H=Toeplitz(0,1,-1), in which the central diagonal is defined as ones and the T upper diagonal is defined as minus ones. @@ -121,11 +116,13 @@ def toeplitz_matrix(T: int, dimension: int) -> NDArray: ------- NDArray Sparse Toeplitz matrix using scipy format - """ + """ n_lags = dimension - T diagonals = [np.ones(n_lags), -np.ones(n_lags)] - H_top = sps.diags(diagonals, offsets=[0, T], shape=(n_lags, dimension), format="csr") + H_top = sps.diags( + diagonals, offsets=[0, T], shape=(n_lags, dimension), format="csr" + ) H = sps.dok_matrix((dimension, dimension)) H[:n_lags] = H_top return H diff --git a/qolmat/imputations/softimpute.py b/qolmat/imputations/softimpute.py index 5d04b39b..72d3a8c4 100644 --- a/qolmat/imputations/softimpute.py +++ b/qolmat/imputations/softimpute.py @@ -1,19 +1,22 @@ +"""Script for SoftImpute class.""" + from __future__ import annotations -from typing import Optional, Tuple, Union import warnings +from typing import Optional, Tuple, Union import numpy as np from numpy.typing import NDArray from sklearn import utils as sku from sklearn.base import BaseEstimator, TransformerMixin -from qolmat.utils import utils from qolmat.imputations.rpca import rpca_utils +from qolmat.utils import utils class SoftImpute(BaseEstimator, TransformerMixin): - """ + """Class for the Rank Restricted Soft SVD algorithm. + This class implements the Rank Restricted Soft SVD algorithm presented in Hastie, Trevor, et al. "Matrix completion and low-rank SVD via fast alternating least squares." The Journal of Machine Learning @@ -36,7 +39,8 @@ class SoftImpute(BaseEstimator, TransformerMixin): max_iterations : int Maximum number of iterations random_state : int, optional - The seed of the pseudo random number generator to use, for reproductibility + The seed of the pseudo random number generator to use, + for reproductibility verbose : bool flag for verbosity @@ -44,7 +48,9 @@ class SoftImpute(BaseEstimator, TransformerMixin): -------- >>> import numpy as np >>> from qolmat.imputations.softimpute import SoftImpute - >>> D = np.array([[1, 2, np.nan, 4], [1, 5, 3, np.nan], [4, 2, 3, 2], [1, 1, 5, 4]]) + >>> D = np.array( + ... [[1, 2, np.nan, 4], [1, 5, 3, np.nan], [4, 2, 3, 2], [1, 1, 5, 4]] + ... ) >>> Omega = ~np.isnan(D) >>> M, A = SoftImpute(random_state=11).decompose(D, Omega) >>> print(M + A) @@ -52,6 +58,7 @@ class SoftImpute(BaseEstimator, TransformerMixin): [1. 5. 3. 0.87217939] [4. 2. 3. 2. ] [1. 1. 5. 4. ]] + """ def __init__( @@ -73,8 +80,7 @@ def __init__( self.verbose = verbose def get_params_scale(self, X: NDArray): - """ - Get parameters for scaling in Soft Impute based on the input data. + """Get parameters for scaling in Soft Impute based on the input data. Parameters ---------- @@ -98,8 +104,7 @@ def get_params_scale(self, X: NDArray): return dict_params def decompose(self, X: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: - """ - Compute the Soft Impute decomposition + """Compute the Soft Impute decomposition. Parameters ---------- @@ -114,11 +119,13 @@ def decompose(self, X: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: Low-rank signal A: NDArray Anomalies + """ params_scale = self.get_params_scale(X) rank = params_scale["rank"] if self.rank is None else self.rank tau = params_scale["tau"] if self.tau is None else self.tau - assert tau > 0 + if tau <= 0: + raise ValueError(f"Parameter tau has negative value: {tau}") # Step 1 : Initializing n, m = X.shape @@ -138,7 +145,9 @@ def decompose(self, X: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: # Step 2 : Upate on B D2_invreg = (D**2 + tau) ** (-1) - Btilde = ((U * D).T @ np.where(Omega, X - A @ B.T, 0) + (B * D**2).T).T + Btilde = ( + (U * D).T @ np.where(Omega, X - A @ B.T, 0) + (B * D**2).T + ).T Btilde = Btilde * D2_invreg Utilde, D2tilde, _ = np.linalg.svd(Btilde * D, full_matrices=False) @@ -148,7 +157,9 @@ def decompose(self, X: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: # Step 3 : Upate on A D2_invreg = (D**2 + tau) ** (-1) - Atilde = ((V * D).T @ np.where(Omega, X - A @ B.T, 0).T + (A * D**2).T).T + Atilde = ( + (V * D).T @ np.where(Omega, X - A @ B.T, 0).T + (A * D**2).T + ).T Atilde = Atilde * D2_invreg Utilde, D2tilde, _ = np.linalg.svd(Atilde * D, full_matrices=False) @@ -162,7 +173,8 @@ def decompose(self, X: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: print(f"Iteration {iter_}: ratio = {round(ratio, 4)}") if ratio < self.tolerance: print( - f"Convergence reached at iteration {iter_} with ratio = {round(ratio, 4)}" + f"Convergence reached at iteration {iter_} " + f"with ratio = {round(ratio, 4)}" ) break @@ -178,7 +190,9 @@ def decompose(self, X: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: if self.verbose and (cost_end > cost_start + 1e-9): warnings.warn( f"Convergence failed: cost function increased from" - f" {cost_start} to {cost_end} instead of decreasing!".format("%.2f") + f" {cost_start} to {cost_end} instead of decreasing!".format( + "%.2f" + ) ) return M, A @@ -192,7 +206,9 @@ def _check_convergence( D: NDArray, V: NDArray, ) -> float: - """Given a pair of iterates (U_old, D_old, V_old) and (U, D, V), + """Check if the convergence has been reached. + + Given a pair of iterates (U_old, D_old, V_old) and (U, D, V), it computes the relative change in Frobenius norm given by || U_old @ D_old^2 @ V_old.T - U @ D^2 @ V.T ||_F^2 / || U_old @ D_old^2 @ V_old.T ||_F^2 @@ -216,6 +232,7 @@ def _check_convergence( ------- float relative change + """ if any(arg is None for arg in (U_old, D_old, V_old, U, D, V)): raise ValueError("One or more arguments are None.") @@ -261,8 +278,7 @@ def cost_function( Omega: NDArray, tau: float, ): - """ - Compute cost function for different RPCA algorithm + """Compute cost function for different RPCA algorithm. Parameters ---------- @@ -281,6 +297,7 @@ def cost_function( ------- float Value of the cost function minimized by the Soft Impute algorithm + """ norm_frobenius = np.sum(np.where(Omega, X - M, 0) ** 2) norm_nuclear = np.linalg.norm(M, "nuc") diff --git a/qolmat/utils/algebra.py b/qolmat/utils/algebra.py index 9e2af1a6..e78b6bdf 100644 --- a/qolmat/utils/algebra.py +++ b/qolmat/utils/algebra.py @@ -1,6 +1,8 @@ +"""Utils algebra functions for qolmat package.""" + import numpy as np import scipy -from numpy.typing import NDArray, ArrayLike +from numpy.typing import NDArray def frechet_distance_exact( @@ -9,13 +11,18 @@ def frechet_distance_exact( means2: NDArray, cov2: NDArray, ) -> float: - """Compute the Fréchet distance between two dataframes df1 and df2 - Frechet_distance = || mu_1 - mu_2 ||_2^2 + Tr(Sigma_1 + Sigma_2 - 2(Sigma_1 . Sigma_2)^(1/2)) - It is normalized, df1 and df2 are first scaled by a factor (std(df1) + std(df2)) / 2 + """Compute the Fréchet distance between two dataframes df1 and df2. + + Frechet_distance = || mu_1 - mu_2 ||_2^2 + + Tr(Sigma_1 + Sigma_2 - 2(Sigma_1 . Sigma_2)^(1/2)) + It is normalized, df1 and df2 are first scaled + by a factor (std(df1) + std(df2)) / 2 and then centered around (mean(df1) + mean(df2)) / 2 - The result is divided by the number of samples to get an homogeneous result. - Based on: Dowson, D. C., and BV666017 Landau. "The Fréchet distance between multivariate normal - distributions." Journal of multivariate analysis 12.3 (1982): 450-455. + The result is divided by the number of samples to get + an homogeneous result. + Based on: Dowson, D. C., and BV666017 Landau. + "The Fréchet distance between multivariate normal distributions." + Journal of multivariate analysis 12.3 (1982): 450-455. Parameters ---------- @@ -32,9 +39,14 @@ def frechet_distance_exact( ------- float Frechet distance + """ n = len(means1) - if (means2.shape != (n,)) or (cov1.shape != (n, n)) or (cov2.shape != (n, n)): + if ( + (means2.shape != (n,)) + or (cov1.shape != (n, n)) + or (cov2.shape != (n, n)) + ): raise ValueError("Inputs have to be of same dimensions.") ssdiff = np.sum((means1 - means2) ** 2.0) @@ -52,8 +64,9 @@ def frechet_distance_exact( def kl_divergence_gaussian_exact( means1: NDArray, cov1: NDArray, means2: NDArray, cov2: NDArray ) -> float: - """ - Exact Kullback-Leibler divergence computed between two multivariate normal distributions + """Compute the exact Kullback-Leibler divergence. + + This is computed between two multivariate normal distributions Based on https://en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence Parameters @@ -66,10 +79,12 @@ def kl_divergence_gaussian_exact( Mean of the second distribution cov2: NDArray Covariance matrx of the second distribution + Returns ------- float Kulback-Leibler divergence + """ n_variables = len(means1) L1, _ = scipy.linalg.cho_factor(cov1) diff --git a/qolmat/utils/data.py b/qolmat/utils/data.py index 2adecf4e..1ae46f34 100644 --- a/qolmat/utils/data.py +++ b/qolmat/utils/data.py @@ -1,9 +1,11 @@ +"""Utils data for qolmat package.""" + import os import sys import zipfile from datetime import datetime from math import pi -from typing import List, Tuple, Union +from typing import Dict, List, Tuple, Union from urllib import request import numpy as np @@ -16,28 +18,33 @@ def read_csv_local(data_file_name: str, **kwargs) -> pd.DataFrame: - """Load csv files + """Load csv files. Parameters ---------- data_file_name : str - Filename. Has to be "beijing" or "conductors" - kwargs : dict + Filename. Has to be "beijing" or "conductors". + **kwargs : dict, optional + Additional keyword arguments passed to `pandas.read_csv`. Returns ------- df : pd.DataFrame dataframe + """ - df = pd.read_csv(os.path.join(ROOT_DIR, "data", f"{data_file_name}.csv"), **kwargs) + df = pd.read_csv( + os.path.join(ROOT_DIR, "data", f"{data_file_name}.csv"), **kwargs + ) return df def download_data_from_zip( zipname: str, urllink: str, datapath: str = "data/" ) -> List[pd.DataFrame]: - """ - Downloads and extracts ZIP files from a URL, then loads DataFrames from CSV files. + """Download and extracts ZIP files from a URL. + + It also loads DataFrames from CSV files. Parameters ---------- @@ -52,7 +59,9 @@ def download_data_from_zip( Returns ------- List[pd.DataFrame] - A list of DataFrames loaded from the CSV files within the extracted directory. + A list of DataFrames loaded from the CSV files + within the extracted directory. + """ path_zip = os.path.join(datapath, zipname) path_zip_ext = path_zip + ".zip" @@ -68,9 +77,11 @@ def download_data_from_zip( def get_dataframes_in_folder(path: str, extension: str) -> List[pd.DataFrame]: - """ - Loads all dataframes from files with a specified extension within a directory, including - subdirectories. Special handling for '.tsf' files which are converted and immediately returned. + """Load all dataframes from files. + + Loads all files with a specified extension within a directory, including + subdirectories. Special handling for '.tsf' files which are converted + and immediately returned. Parameters ---------- @@ -82,8 +93,10 @@ def get_dataframes_in_folder(path: str, extension: str) -> List[pd.DataFrame]: Returns ------- List[pd.DataFrame] - A list of pandas DataFrames loaded from the files matching the extension. - If a '.tsf' file is found, its converted DataFrame is returned immediately. + A list of pandas DataFrames loaded from the files + matching the extension. If a '.tsf' file is found, + its converted DataFrame is returned immediately. + """ list_df = [] for folder, _, files in os.walk(path): @@ -91,7 +104,9 @@ def get_dataframes_in_folder(path: str, extension: str) -> List[pd.DataFrame]: if extension in file: list_df.append(pd.read_csv(os.path.join(folder, file))) if ".tsf" in file: - loaded_data = convert_tsf_to_dataframe(os.path.join(folder, file)) + loaded_data = convert_tsf_to_dataframe( + os.path.join(folder, file) + ) return [loaded_data] return list_df @@ -103,8 +118,7 @@ def generate_artificial_ts( ratio_anomalies: float, amp_noise: float, ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """ - Generates time series data, anomalies, and noise based on given parameters. + """Generate TS data, anomalies, and noise based on given parameters. Parameters ---------- @@ -125,8 +139,8 @@ def generate_artificial_ts( Time series data with sine waves (X). Anomaly data with specified amplitudes at random positions (A). Gaussian noise added to the time series (E). - """ + """ mesh = np.arange(n_samples) X = np.ones(n_samples) for p in periods: @@ -135,7 +149,9 @@ def generate_artificial_ts( n_anomalies = int(n_samples * ratio_anomalies) anomalies = np.random.standard_exponential(size=n_anomalies) anomalies *= amp_anomalies * np.random.choice([-1, 1], size=n_anomalies) - ind_anomalies = np.random.choice(range(n_samples), size=n_anomalies, replace=False) + ind_anomalies = np.random.choice( + range(n_samples), size=n_anomalies, replace=False + ) A = np.zeros(n_samples) A[ind_anomalies] = anomalies @@ -148,21 +164,23 @@ def get_data( datapath: str = "data/", n_groups_max: int = sys.maxsize, ) -> pd.DataFrame: - """ - Download or generate data + """Download or generate data. Parameters ---------- + name_data: str, optional + name of the file, by default "Beijing" datapath : str, optional data path, by default "data/" - download : bool, optional - if True: download a public dataset, if False: generate random univariate time series, by - default True + n_groups_max : int, optional + max number of groups, by default sys.maxsize. + Only used if name_data == "SNCF" Returns ------- pd.DataFrame requested data + """ url_zenodo = "https://zenodo.org/record/" if name_data == "Beijing": @@ -178,7 +196,9 @@ def get_data( path = "https://gist.githubusercontent.com/fyyying/4aa5b471860321d7b47fd881898162b7/raw/" "6907bb3a38bfbb6fccf3a8b1edfb90e39714d14f/titanic_dataset.csv" df = pd.read_csv(path) - df = df[["Survived", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked"]] + df = df[ + ["Survived", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked"] + ] df["Age"] = pd.to_numeric(df["Age"], errors="coerce") df["Fare"] = pd.to_numeric(df["Fare"], errors="coerce") return df @@ -194,7 +214,9 @@ def get_data( n_samples, periods, amp_anomalies, ratio_anomalies, amp_noise ) signal = X + A + E - df = pd.DataFrame({"signal": signal, "index": range(n_samples), "station": city}) + df = pd.DataFrame( + {"signal": signal, "index": range(n_samples), "station": city} + ) df.set_index(["station", "index"], inplace=True) df["X"] = X @@ -206,7 +228,9 @@ def get_data( df = pd.read_parquet(path_file) sizes_stations = df.groupby("station")["val_in"].mean().sort_values() n_groups_max = min(len(sizes_stations), n_groups_max) - stations = sizes_stations.index.get_level_values("station").unique()[-n_groups_max:] + stations = sizes_stations.index.get_level_values("station").unique()[ + -n_groups_max: + ] df = df.loc[stations] return df elif name_data == "Beijing_online": @@ -227,20 +251,30 @@ def get_data( df = pd.read_csv(csv_url, index_col=0) return df elif name_data == "Monach_weather": - urllink = os.path.join(url_zenodo, "4654822/files/weather_dataset.zip?download=1") + urllink = os.path.join( + url_zenodo, "4654822/files/weather_dataset.zip?download=1" + ) zipname = "weather_dataset" - list_loaded_data = download_data_from_zip(zipname, urllink, datapath=datapath) + list_loaded_data = download_data_from_zip( + zipname, urllink, datapath=datapath + ) loaded_data = list_loaded_data[0] df_list: List[pd.DataFrame] = [] for k in range(len(loaded_data)): values = list(loaded_data["series_value"][k]) freq = "1D" time_index = pd.date_range( - start=pd.Timestamp("01/01/2010"), periods=len(values), freq=freq + start=pd.Timestamp("01/01/2010"), + periods=len(values), + freq=freq, ) df_list = df_list + [ pd.DataFrame( - {loaded_data.series_name[k] + " " + loaded_data.series_type[k]: values}, + { + loaded_data.series_name[k] + + " " + + loaded_data.series_type[k]: values + }, index=time_index, ) ] @@ -254,18 +288,26 @@ def get_data( "4659727/files/australian_electricity_demand_dataset.zip?download=1", ) zipname = "australian_electricity_demand_dataset" - list_loaded_data = download_data_from_zip(zipname, urllink, datapath=datapath) + list_loaded_data = download_data_from_zip( + zipname, urllink, datapath=datapath + ) loaded_data = list_loaded_data[0] df_list = [] for k in range(len(loaded_data)): values = list(loaded_data["series_value"][k]) freq = "30min" time_index = pd.date_range( - start=loaded_data.start_timestamp[k], periods=len(values), freq=freq + start=loaded_data.start_timestamp[k], + periods=len(values), + freq=freq, ) df_list = df_list + [ pd.DataFrame( - {loaded_data.series_name[k] + " " + loaded_data.state[k]: values}, + { + loaded_data.series_name[k] + + " " + + loaded_data.state[k]: values + }, index=time_index, ) ] @@ -278,7 +320,7 @@ def get_data( def preprocess_data_beijing(df: pd.DataFrame) -> pd.DataFrame: - """Preprocess data from the "Beijing" datset + """Preprocess data from the "Beijing" datset. Parameters ---------- @@ -289,25 +331,39 @@ def preprocess_data_beijing(df: pd.DataFrame) -> pd.DataFrame: ------- pd.DataFrame preprocessed dataframe + """ df["datetime"] = pd.to_datetime(df[["year", "month", "day", "hour"]]) df["station"] = "Beijing" df.set_index(["station", "datetime"], inplace=True) df.drop( - columns=["year", "month", "day", "hour", "No", "cbwd", "Iws", "Is", "Ir"], + columns=[ + "year", + "month", + "day", + "hour", + "No", + "cbwd", + "Iws", + "Is", + "Ir", + ], inplace=True, ) df.sort_index(inplace=True) df = df.groupby( - ["station", df.index.get_level_values("datetime").floor("d")], group_keys=False + ["station", df.index.get_level_values("datetime").floor("d")], + group_keys=False, ).mean() return df -def add_holes(df: pd.DataFrame, ratio_masked: float, mean_size: int) -> pd.DataFrame: - """ - Creates holes in a dataset with no missing value, starting from `df`. Only used in the - documentation to design examples. +def add_holes( + df: pd.DataFrame, ratio_masked: float, mean_size: int +) -> pd.DataFrame: + """Create holes in a dataset with no missing value, starting from `df`. + + Only used in the documentation to design examples. Parameters ---------- @@ -319,10 +375,12 @@ def add_holes(df: pd.DataFrame, ratio_masked: float, mean_size: int) -> pd.DataF ratio_masked : float Targeted global proportion of nans added in the returned dataset + Returns ------- pd.DataFrame dataframe with missing values + """ groups = df.index.names.difference(["datetime", "date", "index", None]) if groups != []: @@ -334,10 +392,16 @@ def add_holes(df: pd.DataFrame, ratio_masked: float, mean_size: int) -> pd.DataF 1, ratio_masked=ratio_masked, subset=df.columns ) - generator.dict_probas_out = {column: 1 / mean_size for column in df.columns} - generator.dict_ratios = {column: 1 / len(df.columns) for column in df.columns} + generator.dict_probas_out = { + column: 1 / mean_size for column in df.columns + } + generator.dict_ratios = { + column: 1 / len(df.columns) for column in df.columns + } if generator.groups: - mask = df.groupby(groups, group_keys=False).apply(generator.generate_mask) + mask = df.groupby(groups, group_keys=False).apply( + generator.generate_mask + ) else: mask = generator.generate_mask(df) @@ -351,8 +415,10 @@ def get_data_corrupted( mean_size: int = 90, ratio_masked: float = 0.2, ) -> pd.DataFrame: - """ - Returns a dataframe with controled corruption optained from the source `name_data` + """Corrupt data. + + Return a dataframe with controlled corruption obtained + from the source `name_data`. Parameters ---------- @@ -362,10 +428,12 @@ def get_data_corrupted( Mean size of the holes to be generated using a geometric law ratio_masked: float Percent of missing data in each column in the output dataframe + Returns ------- pd.DataFrame Dataframe with missing values + """ df = get_data(name_data) df = add_holes(df, mean_size=mean_size, ratio_masked=ratio_masked) @@ -373,8 +441,7 @@ def get_data_corrupted( def add_station_features(df: pd.DataFrame) -> pd.DataFrame: - """ - Create a station feature in the dataset + """Create a station feature in the dataset. Parameters ---------- @@ -385,6 +452,7 @@ def add_station_features(df: pd.DataFrame) -> pd.DataFrame: ------- pd.DataFrame dataframe with missing values + """ df = df.copy() stations = df.index.get_level_values("station") @@ -393,9 +461,10 @@ def add_station_features(df: pd.DataFrame) -> pd.DataFrame: return df -def add_datetime_features(df: pd.DataFrame, col_time: str = "datetime") -> pd.DataFrame: - """ - Create a seasonal feature in the dataset with a cosine function +def add_datetime_features( + df: pd.DataFrame, col_time: str = "datetime" +) -> pd.DataFrame: + """Create a seasonal feature in the dataset with a cosine function. Parameters ---------- @@ -408,11 +477,14 @@ def add_datetime_features(df: pd.DataFrame, col_time: str = "datetime") -> pd.Da ------- pd.DataFrame dataframe with missing values + """ df = df.copy() time = df.index.get_level_values(col_time).to_series() days_in_year = time.dt.year.apply( - lambda x: 366 if ((x % 4 == 0) and (x % 100 != 0)) or (x % 400 == 0) else 365 + lambda x: 366 + if ((x % 4 == 0) and (x % 100 != 0)) or (x % 400 == 0) + else 365 ) ratio = time.dt.dayofyear.values / days_in_year.values df["time_cos"] = np.cos(2 * np.pi * ratio) @@ -421,13 +493,30 @@ def add_datetime_features(df: pd.DataFrame, col_time: str = "datetime") -> pd.Da def convert_tsf_to_dataframe( - full_file_path_and_name, - replace_missing_vals_with="NaN", - value_column_name="series_value", + full_file_path_and_name: str, + replace_missing_vals_with: Union[str, float, int] = "NaN", + value_column_name: str = "series_value", ): + """Convert a .tsf file to a dataframe. + + Parameters + ---------- + full_file_path_and_name : str + Filename + replace_missing_vals_with : Union[str, float, int], optional + Replace missing values with, by default "NaN" + value_column_name : str, optional + Name of the column containing the values, by default "series_value" + + Returns + ------- + _type_ + _description_ + + """ col_names = [] col_types = [] - all_data = {} + all_data: Dict[str, List] = {} line_count = 0 found_data_tag = False found_data_section = False @@ -443,21 +532,29 @@ def convert_tsf_to_dataframe( line_content = line.split(" ") if line.startswith("@attribute"): if len(line_content) != 3: - raise Exception("Invalid meta-data specification.") + raise Exception( + "Invalid meta-data specification." + ) col_names.append(line_content[1]) col_types.append(line_content[2]) else: if len(line_content) != 2: - raise Exception("Invalid meta-data specification.") + raise Exception( + "Invalid meta-data specification." + ) else: if len(col_names) == 0: - raise Exception("Attribute section must come before data.") + raise Exception( + "Attribute section must come before data." + ) found_data_tag = True elif not line.startswith("#"): if len(col_names) == 0: - raise Exception(" Attribute section must come before data.") + raise Exception( + " Attribute section must come before data." + ) elif not found_data_tag: raise Exception("Missing @data tag.") else: @@ -472,25 +569,35 @@ def convert_tsf_to_dataframe( full_info = line.split(":") if len(full_info) != (len(col_names) + 1): - raise Exception("Missing attributes/values in series.") + raise Exception( + "Missing attributes/values in series." + ) series = full_info[len(full_info) - 1] - series = series.split(",") + series = series.split(",") # type: ignore if len(series) == 0: - raise Exception(" Missing values should be indicated with ? symbol") + raise Exception( + " Missing values should be indicated " + "with ? symbol" + ) numeric_series = [] for val in series: if val == "?": - numeric_series.append(replace_missing_vals_with) + numeric_series.append( + replace_missing_vals_with + ) else: - numeric_series.append(float(val)) + numeric_series.append(float(val)) # type: ignore - if numeric_series.count(replace_missing_vals_with) == len(numeric_series): + if numeric_series.count( + replace_missing_vals_with + ) == len(numeric_series): raise Exception( - "At least one numeric value should be there in a series." + "At least one numeric value should be " + "there in a series." ) all_series.append(pd.Series(numeric_series).array) @@ -500,9 +607,12 @@ def convert_tsf_to_dataframe( if col_types[i] == "numeric": att_val = int(full_info[i]) elif col_types[i] == "string": - att_val = str(full_info[i]) + att_val = str(full_info[i]) # type: ignore elif col_types[i] == "date": - att_val = datetime.strptime(full_info[i], "%Y-%m-%d %H-%M-%S") + att_val = datetime.strptime( + full_info[i], + "%Y-%m-%d %H-%M-%S", # type: ignore + ) else: raise Exception("Invalid attribute type.") diff --git a/qolmat/utils/exceptions.py b/qolmat/utils/exceptions.py index 513e843b..baddfb38 100644 --- a/qolmat/utils/exceptions.py +++ b/qolmat/utils/exceptions.py @@ -1,7 +1,11 @@ +"""Exceptions for qolmat package.""" + from typing import Any, List, Tuple, Type class PyTorchExtraNotInstalled(Exception): + """Raise when pytorch extra is not installed.""" + def __init__(self): super().__init__( """Please install torch xx.xx.xx @@ -10,6 +14,8 @@ def __init__(self): class SignalTooShort(Exception): + """Raise when the signal is too short.""" + def __init__(self, period: int, n_cols: int): super().__init__( f"""`period` must be smaller than the signals duration. @@ -18,6 +24,8 @@ def __init__(self, period: int, n_cols: int): class NoMissingValue(Exception): + """Raise an error when there is no missing value.""" + def __init__(self, subset_without_nans: List[str]): super().__init__( f"No missing value in the columns {subset_without_nans}! " @@ -26,47 +34,78 @@ def __init__(self, subset_without_nans: List[str]): class SubsetIsAString(Exception): + """Raise an error when the subset is a string.""" + def __init__(self, subset: Any): - super().__init__(f"Provided subset `{subset}` should be None or a list!") + super().__init__( + f"Provided subset `{subset}` should be None or a list!" + ) class NotDimension2(Exception): + """Raise an error when the matrix is not of dim 2.""" + def __init__(self, shape: Tuple[int, ...]): - super().__init__(f"Provided matrix is of shape {shape}, which is not of dimension 2!") + super().__init__( + f"Provided matrix is of shape {shape}, " + "which is not of dimension 2!" + ) class NotDataFrame(Exception): + """Raise an error when the input is not a dataframe.""" + def __init__(self, X_type: Type[Any]): - super().__init__(f"Input musr be a dataframe, not a {X_type}") + super().__init__(f"Input must be a dataframe, not a {X_type}") class NotEnoughSamples(Exception): + """Raise an error when there is no not enough samples.""" + def __init__(self, max_num_row: int, min_n_rows: int): super().__init__( - f"Not enough valid patterns found. Largest found pattern has {max_num_row} rows, when " + f"Not enough valid patterns found. " + f"Largest found pattern has {max_num_row} rows, when " f"they should have at least min_n_rows={min_n_rows}." ) class EstimatorNotDefined(Exception): + """Raise an error when the estimator is not defined.""" + def __init__(self): - super().__init__("The underlying estimator should be defined beforehand!") + super().__init__( + "The underlying estimator should be defined beforehand!" + ) class SingleSample(Exception): + """Raise an error when there is a single sample.""" + def __init__(self): - super().__init__("""This imputer cannot be fitted on a single sample!""") + super().__init__( + """This imputer cannot be fitted on a single sample!""" + ) class IllConditioned(Exception): + """Raise an error when the covariance matrix is ill-conditioned.""" + def __init__(self, min_sv: float, min_std: float): super().__init__( - f"The covariance matrix is ill-conditioned, indicating high-colinearity: the smallest " - f"singular value of the data matrix is smaller than the threshold min_std ({min_sv} < " - f"{min_std}). Consider removing columns of decreasing the threshold." + f"The covariance matrix is ill-conditioned, " + "indicating high-colinearity: " + "the smallest singular value of the data matrix is smaller " + f"than the threshold min_std ({min_sv} < {min_std}). " + f"Consider removing columns of decreasing the threshold." ) class TypeNotHandled(Exception): + """Raise an error when the type is not handled.""" + def __init__(self, col: str, type_col: str): - super().__init__(f"The column `{col}` is of type `{type_col}`, which is not handled!") + super().__init__( + f"The column `{col}` is of type `{type_col}`, " + "which is not handled!" + ) diff --git a/qolmat/utils/plot.py b/qolmat/utils/plot.py index c6700e13..e9809425 100644 --- a/qolmat/utils/plot.py +++ b/qolmat/utils/plot.py @@ -1,18 +1,17 @@ -""" -Useful drawing functions -""" +"""Useful drawing functions.""" from __future__ import annotations -from typing import Dict, List, Any, Optional, Tuple, Union + +from typing import Any, Dict, List, Optional, Tuple, Union import matplotlib as mpl import matplotlib.pyplot as plt import matplotlib.ticker as plticker import numpy as np -from numpy.typing import NDArray import pandas as pd import scipy from mpl_toolkits.axes_grid1 import make_axes_locatable +from numpy.typing import NDArray plt.rcParams["axes.spines.right"] = False plt.rcParams["axes.spines.top"] = False @@ -23,18 +22,20 @@ tab10 = plt.get_cmap("tab10") -def plot_matrices(list_matrices: List[np.ndarray], title: Optional[str] = None) -> None: - """Plot RPCA matrices +def plot_matrices( + list_matrices: List[np.ndarray], title: Optional[str] = None +) -> None: + """Plot RPCA matrices. Parameters ---------- list_matrices : List[np.ndarray] - List containing, in the right order, the observations matrix, the low-rank matrix and the - sparse matrix + List containing, in the right order, the observations matrix, + the low-rank matrix and the sparse matrix title : Optional[str], optional if present, title of the saved figure, by default None - """ + """ suptitles = ["Observations", "Low-rank", "Sparse"] fig, ax = plt.subplots(1, 3, figsize=(10, 3)) @@ -62,21 +63,21 @@ def plot_signal( ylabel: Optional[str] = None, dates: Optional[List] = None, ) -> None: - """Plot RPCA results for time series + """Plot RPCA results for time series. Parameters ---------- list_signals : List[List] - List containing, in the right order, the observed time series, the cleaned signal and - the anomalies + List containing, in the right order, the observed time series, + the cleaned signal and the anomalies title : Optional[str], optional if present, title of the saved figure, by default None ylabel : Optional[str], optional ylabel, by default None dates : Optional[List], optional dates of the time series (xlabel), by default None - """ + """ suptitles = ["Observations", "Cleaned", "Anomalies"] colors = ["black", "darkblue", "crimson"] fontsize = 15 @@ -106,7 +107,7 @@ def plot_images( dims: Tuple[int, int], filename: Optional[str] = None, ) -> None: - """Plot multiple images in 3 columns for original, background and "foreground" + """Plot multiple images for original, background and "foreground". Parameters ---------- @@ -122,8 +123,8 @@ def plot_images( dimensions of the reduction filename : Optional[str], optional filename for saving figure, by default None - """ + """ f = plt.figure(figsize=(15, 10)) r = len(index_array) @@ -163,8 +164,7 @@ def make_ellipses( n_std: float = 2, color: Union[str, Any, Tuple[float, float, float]] = "None", ): - """ - Create a plot of the covariance confidence ellipse of *x* and *y*. + """Create a plot of the covariance confidence ellipse of *x* and *y*. Parameters ---------- @@ -186,16 +186,21 @@ def make_ellipses( Returns ------- matplotlib.patches.Ellipse - """ + """ pearson = cov[0, 1] / np.sqrt(cov[0, 0] * cov[1, 1]) ell_radius_x = np.sqrt(1 + pearson) * 2.5 ell_radius_y = np.sqrt(1 - pearson) * 2.5 - ell = mpl.patches.Ellipse((0, 0), width=ell_radius_x, height=ell_radius_y, facecolor=color) + ell = mpl.patches.Ellipse( + (0, 0), width=ell_radius_x, height=ell_radius_y, facecolor=color + ) scale_x = np.sqrt(cov[0, 0]) * n_std scale_y = np.sqrt(cov[1, 1]) * n_std transf = ( - mpl.transforms.Affine2D().rotate_deg(45).scale(scale_x, scale_y).translate(mean_x, mean_y) + mpl.transforms.Affine2D() + .rotate_deg(45) + .scale(scale_x, scale_y) + .translate(mean_x, mean_y) ) ell.set_transform(transf + ax.transData) ax.add_patch(ell) @@ -211,8 +216,7 @@ def make_ellipses_from_data( n_std: float = 2, color: Union[str, Any, Tuple[float, float, float]] = "None", ): - """ - Create a plot of the covariance confidence ellipse of *x* and *y*. + """Create a plot of the covariance confidence ellipse of *x* and *y*. Parameters ---------- @@ -231,6 +235,7 @@ def make_ellipses_from_data( Returns ------- matplotlib.patches.Ellipse + """ if x.size != y.size: raise ValueError("x and y must be the same size") @@ -248,10 +253,14 @@ def compare_covariances( col_y: str, ax: mpl.axes.Axes, label: str = "", - color: Union[None, str, Tuple[float, float, float], Tuple[float, float, float, float]] = None, + color: Union[ + None, + str, + Tuple[float, float, float], + Tuple[float, float, float, float], + ] = None, ): - """ - Covariance plot: scatter plot with ellipses + """Covariance plot: scatter plot with ellipses. Parameters ---------- @@ -265,12 +274,26 @@ def compare_covariances( variable y, column's name of dataframe df2 to compare with ax : matplotlib.axes._subplots.AxesSubplot matplotlib ax handles + label: str + label of the plot + color: Union[None, str, Tuple[float, float, float], + Tuple[float, float, float, float]] + color of the ellipse + """ df1 = df_1.dropna() df2 = df_2.dropna() if color is None: color = tab10(0) - ax.scatter(df2[col_x], df2[col_y], marker=".", color=color, s=2, alpha=0.7, label="imputed") + ax.scatter( + df2[col_x], + df2[col_y], + marker=".", + color=color, + s=2, + alpha=0.7, + label="imputed", + ) ax.scatter( df1[col_x], df1[col_y], @@ -293,7 +316,9 @@ def multibar( colors: Any = None, decimals: float = 0, ): - """Create a multi-bar graph to represent the values of the different dataframe columns. + """Create a multi-bar graph. + + It represents the values of the different dataframe columns. Parameters ---------- @@ -307,8 +332,8 @@ def multibar( color in multibar plot, by default None decimals : float, optional the decimals numbers, by default 0 - """ + """ if ax is None: ax = plt.gca() if colors is None: @@ -346,8 +371,10 @@ def multibar( plt.legend(loc=(1, 0)) -def plot_imputations(df: pd.DataFrame, dict_df_imputed: Dict[str, pd.DataFrame]): - """Plot original and imputed dataframes for each imputers +def plot_imputations( + df: pd.DataFrame, dict_df_imputed: Dict[str, pd.DataFrame] +): + """Plot original and imputed dataframes for each imputers. Parameters ---------- @@ -355,6 +382,7 @@ def plot_imputations(df: pd.DataFrame, dict_df_imputed: Dict[str, pd.DataFrame]) original dataframe dict_df_imputed : Dict[str, pd.DataFrame] dictionnary of imputed dataframe for each imputers + """ n_columns = len(df.columns) n_imputers = len(dict_df_imputed) @@ -369,7 +397,9 @@ def plot_imputations(df: pd.DataFrame, dict_df_imputed: Dict[str, pd.DataFrame]) plt.plot(values_orig, ".", color="black", label="original") values_imp = df_imputed[col].copy() values_imp[values_orig.notna()] = np.nan - plt.plot(values_imp, ".", color=tab10(0), label=name_imputer, alpha=1) + plt.plot( + values_imp, ".", color=tab10(0), label=name_imputer, alpha=1 + ) plt.ylabel(col, fontsize=16) if i_plot % n_columns == 0: plt.legend(loc=[1, 0], fontsize=18) diff --git a/qolmat/utils/utils.py b/qolmat/utils/utils.py index ce8f7865..34ccfd7e 100644 --- a/qolmat/utils/utils.py +++ b/qolmat/utils/utils.py @@ -1,23 +1,24 @@ -from typing import List, Optional, Tuple, Union -import warnings +"""Utils for qolmat package.""" + +from typing import List, Tuple, Union import numpy as np import pandas as pd - from numpy.typing import NDArray from sklearn.base import check_array -from qolmat.utils.exceptions import NotDimension2, SignalTooShort +from qolmat.utils.exceptions import NotDimension2 HyperValue = Union[int, float, str] def _get_numerical_features(df1: pd.DataFrame) -> List[str]: - """Get numerical features from dataframe + """Get numerical features from dataframe. Parameters ---------- df1 : pd.DataFrame + Input dataframe. Returns ------- @@ -28,6 +29,7 @@ def _get_numerical_features(df1: pd.DataFrame) -> List[str]: ------ Exception No numerical feature is found + """ cols_numerical = df1.select_dtypes(include=np.number).columns.tolist() if len(cols_numerical) == 0: @@ -37,11 +39,12 @@ def _get_numerical_features(df1: pd.DataFrame) -> List[str]: def _get_categorical_features(df1: pd.DataFrame) -> List[str]: - """Get categorical features from dataframe + """Get categorical features from dataframe. Parameters ---------- df1 : pd.DataFrame + Input dataframe. Returns ------- @@ -52,10 +55,12 @@ def _get_categorical_features(df1: pd.DataFrame) -> List[str]: ------ Exception No categorical feature is found - """ + """ cols_numerical = df1.select_dtypes(include=np.number).columns.tolist() - cols_categorical = [col for col in df1.columns.to_list() if col not in cols_numerical] + cols_categorical = [ + col for col in df1.columns.to_list() if col not in cols_numerical + ] if len(cols_categorical) == 0: raise Exception("No categorical feature is found.") else: @@ -63,9 +68,10 @@ def _get_categorical_features(df1: pd.DataFrame) -> List[str]: def _validate_input(X: NDArray) -> pd.DataFrame: - """ - Checks that the input X can be converted into a DataFrame, and returns the corresponding - dataframe. + """Calidate the input array. + + Checks that the input X can be converted into a DataFrame, + and returns the corresponding dataframe. Parameters ---------- @@ -75,8 +81,9 @@ def _validate_input(X: NDArray) -> pd.DataFrame: Returns ------- pd.DataFrame - Formatted dataframe, if the input had no column names then the dataframe columns are - integers + Formatted dataframe, if the input had no column names + then the dataframe columns are integers + """ check_array(X, force_all_finite="allow-nan", dtype=None) if not isinstance(X, pd.DataFrame): @@ -85,7 +92,7 @@ def _validate_input(X: NDArray) -> pd.DataFrame: raise ValueError if len(X_np.shape) == 1: X_np = X_np.reshape(-1, 1) - df = pd.DataFrame(X_np, columns=[i for i in range(X_np.shape[1])]) + df = pd.DataFrame(X_np, columns=list(range(X_np.shape[1]))) df = df.infer_objects() else: df = X @@ -103,7 +110,7 @@ def progress_bar( length: int = 100, fill: str = "█", ): - """Call in a loop to create terminal progress bar + """Call in a loop to create terminal progress bar. Parameters ---------- @@ -121,8 +128,11 @@ def progress_bar( character length of bar, by default 100 fill : str bar fill character, by default "█" + """ - percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) + percent = ("{0:." + str(decimals) + "f}").format( + 100 * (iteration / float(total)) + ) filled_length = int(length * iteration // total) bar = fill * filled_length + "-" * (length - filled_length) print(f"\r{prefix} |{bar}| {percent}% {suffix}", end="\r") @@ -131,7 +141,7 @@ def progress_bar( def acf(values: pd.Series, lag_max: int = 30) -> pd.Series: - """Correlation series of dataseries + """Correlation series of dataseries. Parameters ---------- @@ -144,6 +154,7 @@ def acf(values: pd.Series, lag_max: int = 30) -> pd.Series: ------- pd.Series correlation series of value + """ acf = pd.Series(0, index=range(lag_max)) for lag in range(lag_max): @@ -152,8 +163,7 @@ def acf(values: pd.Series, lag_max: int = 30) -> pd.Series: def impute_nans(M: NDArray, method: str = "zeros") -> NDArray: - """ - Impute the M's nan with the specified method + """Impute the M's nan with the specified method. Parameters ---------- @@ -166,6 +176,7 @@ def impute_nans(M: NDArray, method: str = "zeros") -> NDArray: ------- NDArray Imputed Array + Raises ------ ValueError @@ -180,9 +191,13 @@ def impute_nans(M: NDArray, method: str = "zeros") -> NDArray: isna = np.isnan(values) nna = np.sum(isna) if method == "mean": - value_imputation = np.nanmean(M) if nna == n_rows else np.nanmean(values) + value_imputation = ( + np.nanmean(M) if nna == n_rows else np.nanmean(values) + ) elif method == "median": - value_imputation = np.nanmedian(M) if nna == n_rows else np.nanmedian(values) + value_imputation = ( + np.nanmedian(M) if nna == n_rows else np.nanmedian(values) + ) elif method == "zeros": value_imputation = 0 else: @@ -193,8 +208,7 @@ def impute_nans(M: NDArray, method: str = "zeros") -> NDArray: def linear_interpolation(X: NDArray) -> NDArray: - """ - Impute missing data with a linear interpolation, column-wise + """Impute missing data with a linear interpolation, column-wise. Parameters ---------- @@ -205,6 +219,7 @@ def linear_interpolation(X: NDArray) -> NDArray: ------- X_interpolated : NDArray imputed array, by linear interpolation + """ n_rows, n_cols = X.shape indices = np.arange(n_rows) @@ -224,12 +239,12 @@ def linear_interpolation(X: NDArray) -> NDArray: def fold_signal(X: NDArray, period: int) -> NDArray: - """ - Reshape a time series into a 2D-array + """Reshape a time series into a 2D-array. Parameters ---------- X : NDArray + Input array to be reshaped. period : int Period used to fold the signal of the 2D-array @@ -242,6 +257,7 @@ def fold_signal(X: NDArray, period: int) -> NDArray: ------ ValueError if X is not a 1D array + """ if len(X.shape) != 2: raise NotDimension2(X.shape) @@ -257,8 +273,20 @@ def fold_signal(X: NDArray, period: int) -> NDArray: def prepare_data(X: NDArray, period: int = 1) -> NDArray: - """ - Transform signal to 2D-array in case of 1D-array. + """Reshape a time series into a 2D-array. + + Parameters + ---------- + X : NDArray + Input array to be reshaped. + period : int, optional + Period used to fold the signal. Defaults to 1. + + Returns + ------- + NDArray + Reshaped array. + """ if len(X.shape) == 1: X = X.reshape(-1, 1) @@ -267,27 +295,43 @@ def prepare_data(X: NDArray, period: int = 1) -> NDArray: return X_fold -def get_shape_original(M: NDArray, shape: tuple) -> NDArray: +def get_shape_original(M: NDArray, shape: Tuple[int, int]) -> NDArray: """Shapes an output matrix from the RPCA algorithm into the original shape. Parameters ---------- M : NDArray Matrix to reshape - X : NDArray - Matrix of the desired shape + shape : Tuple[int, int] + Desired shape Returns ------- NDArray Reshaped matrix + """ - size = np.prod(shape) + size: int = int(np.prod(shape)) M_flat = M.flatten()[:size] return M_flat.reshape(shape) def create_lag_matrices(X: NDArray, p: int) -> Tuple[NDArray, NDArray]: + """Create lag matrices for the VAR(p). + + Parameters + ---------- + X : NDArray + Input matrix + p : int + Number of lags + + Returns + ------- + Tuple[NDArray, NDArray] + Z and Y + + """ n_rows, _ = X.shape n_rows_new = n_rows - p list_X_lag = [np.ones((n_rows_new, 1))] @@ -301,6 +345,19 @@ def create_lag_matrices(X: NDArray, p: int) -> Tuple[NDArray, NDArray]: def nan_mean_cov(X: NDArray) -> Tuple[NDArray, NDArray]: + """Compute mean and covariance matrix. + + Parameters + ---------- + X : NDArray + Input matrix + + Returns + ------- + Tuple[NDArray, NDArray] + Means and covariance matrix + + """ _, n_variables = X.shape means = np.nanmean(X, axis=0) cov = np.ma.cov(np.ma.masked_invalid(X), rowvar=False).data diff --git a/tests/analysis/test_holes_characterization.py b/tests/analysis/test_holes_characterization.py index c794b94e..a77ecbb1 100644 --- a/tests/analysis/test_holes_characterization.py +++ b/tests/analysis/test_holes_characterization.py @@ -11,7 +11,9 @@ @pytest.fixture def mcar_df() -> pd.DataFrame: rng = np.random.default_rng(42) - matrix = rng.multivariate_normal(mean=[0, 0], cov=[[1, 0], [0, 1]], size=200) + matrix = rng.multivariate_normal( + mean=[0, 0], cov=[[1, 0], [0, 1]], size=200 + ) df = pd.DataFrame(data=matrix, columns=["Column_1", "Column_2"]) hole_gen = UniformHoleGenerator( n_splits=1, random_state=42, subset=["Column_2"], ratio_masked=0.2 @@ -23,7 +25,9 @@ def mcar_df() -> pd.DataFrame: @pytest.fixture def mar_hm_df() -> pd.DataFrame: rng = np.random.default_rng(42) - matrix = rng.multivariate_normal(mean=[0, 0], cov=[[1, 0], [0, 1]], size=200) + matrix = rng.multivariate_normal( + mean=[0, 0], cov=[[1, 0], [0, 1]], size=200 + ) quantile_95 = norm.ppf(0.975) df = pd.DataFrame(matrix, columns=["Column_1", "Column_2"]) @@ -37,7 +41,9 @@ def mar_hm_df() -> pd.DataFrame: @pytest.fixture def mar_hc_df() -> pd.DataFrame: rng = np.random.default_rng(42) - matrix = rng.multivariate_normal(mean=[0, 0], cov=[[1, 0], [0, 1]], size=200) + matrix = rng.multivariate_normal( + mean=[0, 0], cov=[[1, 0], [0, 1]], size=200 + ) quantile_95 = norm.ppf(0.975) df = pd.DataFrame(matrix, columns=["Column_1", "Column_2"]) @@ -49,7 +55,8 @@ def mar_hc_df() -> pd.DataFrame: @pytest.mark.parametrize( - "df_input, expected", [("mcar_df", True), ("mar_hm_df", False), ("mar_hc_df", True)] + "df_input, expected", + [("mcar_df", True), ("mar_hm_df", False), ("mar_hc_df", True)], ) def test_little_mcar_test(df_input: pd.DataFrame, expected: bool, request): mcar_test_little = LittleTest(random_state=42) diff --git a/tests/benchmark/test_comparator.py b/tests/benchmark/test_comparator.py index bddb29a3..02971bbb 100644 --- a/tests/benchmark/test_comparator.py +++ b/tests/benchmark/test_comparator.py @@ -1,8 +1,8 @@ -import pytest +from unittest.mock import MagicMock, patch + import numpy as np import pandas as pd -from unittest.mock import patch, MagicMock from qolmat.benchmark.comparator import Comparator generator_holes_mock = MagicMock() @@ -20,7 +20,9 @@ imputer_mock = MagicMock() expected_get_errors = pd.Series( [1.0, 1.0, 1.0, 1.0], - index=pd.MultiIndex.from_tuples([("mae", "A"), ("mae", "B"), ("mse", "A"), ("mse", "B")]), + index=pd.MultiIndex.from_tuples( + [("mae", "A"), ("mae", "B"), ("mse", "A"), ("mse", "B")] + ), ) @@ -28,10 +30,14 @@ def test_get_errors(mock_get_metric): df_origin = pd.DataFrame({"A": [1, np.nan, 3], "B": [np.nan, 5, 6]}) df_imputed = pd.DataFrame({"A": [1, 2, 4], "B": [4, 5, 7]}) - df_mask = pd.DataFrame({"A": [False, False, True], "B": [False, False, True]}) + df_mask = pd.DataFrame( + {"A": [False, False, True], "B": [False, False, True]} + ) - mock_get_metric.return_value = lambda df_origin, df_imputed, df_mask: pd.Series( - [1.0, 1.0], index=["A", "B"] + mock_get_metric.return_value = ( + lambda df_origin, df_imputed, df_mask: pd.Series( + [1.0, 1.0], index=["A", "B"] + ) ) errors = comparator.get_errors(df_origin, df_imputed, df_mask) pd.testing.assert_series_equal(errors, expected_get_errors) @@ -65,7 +71,10 @@ def test_compare(mock_evaluate_errors_sample): errors_imputer1 = pd.Series([0.1, 0.2], index=["mae", "mse"]) errors_imputer2 = pd.Series([0.3, 0.4], index=["mae", "mse"]) - mock_evaluate_errors_sample.side_effect = [errors_imputer1, errors_imputer2] + mock_evaluate_errors_sample.side_effect = [ + errors_imputer1, + errors_imputer2, + ] df_errors = comparator.compare(df_test) assert mock_evaluate_errors_sample.call_count == 2 diff --git a/tests/benchmark/test_hyperparameters.py b/tests/benchmark/test_hyperparameters.py index 5c6ff85a..cf5b567d 100644 --- a/tests/benchmark/test_hyperparameters.py +++ b/tests/benchmark/test_hyperparameters.py @@ -1,20 +1,24 @@ -from typing import Callable, Dict, List, Optional, Tuple, Union +from typing import List, Optional, Tuple, Union +import hyperopt as ho import numpy as np import pandas as pd -import pytest from qolmat.benchmark import hyperparameters -from qolmat.benchmark.hyperparameters import HyperValue # from hyperparameters import HyperValue -from qolmat.benchmark.missing_patterns import _HoleGenerator, EmpiricalHoleGenerator -from qolmat.imputations.imputers import _Imputer, ImputerRpcaNoisy - -import hyperopt as ho +from qolmat.benchmark.missing_patterns import ( + EmpiricalHoleGenerator, + _HoleGenerator, +) +from qolmat.imputations.imputers import ImputerRpcaNoisy, _Imputer -df_origin = pd.DataFrame({"col1": [0, np.nan, 2, 4, np.nan], "col2": [-1, np.nan, 0.5, 1, 1.5]}) -df_imputed = pd.DataFrame({"col1": [0, 1, 2, 3.5, 4], "col2": [-1.5, 0, 1.5, 2, 1.5]}) +df_origin = pd.DataFrame( + {"col1": [0, np.nan, 2, 4, np.nan], "col2": [-1, np.nan, 0.5, 1, 1.5]} +) +df_imputed = pd.DataFrame( + {"col1": [0, 1, 2, 3.5, 4], "col2": [-1.5, 0, 1.5, 2, 1.5]} +) df_mask = pd.DataFrame( { "col1": [False, False, True, False, False], @@ -24,7 +28,9 @@ df_corrupted = df_origin.copy() df_corrupted[df_mask] = np.nan -imputer_rpca = ImputerRpcaNoisy(tau=2, random_state=42, columnwise=True, period=1) +imputer_rpca = ImputerRpcaNoisy( + tau=2, random_state=42, columnwise=True, period=1 +) dict_imputers_rpca = {"rpca": imputer_rpca} generator_holes = EmpiricalHoleGenerator(n_splits=1, ratio_masked=0.5) dict_config_opti = { @@ -41,27 +47,38 @@ class ImputerTest(_Imputer): + """Group tests for Imputer.""" + def __init__( self, groups: Tuple[str, ...] = (), random_state: Union[None, int, np.random.RandomState] = None, value: float = 0, ) -> None: - super().__init__(groups=groups, columnwise=True, random_state=random_state) + """Init function.""" + super().__init__( + groups=groups, columnwise=True, random_state=random_state + ) self.value = value - def _transform_element(self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0): + def _transform_element( + self, df: pd.DataFrame, col: str = "__all__", ngroup: int = 0 + ): df_out = df.copy() df_out = df_out.fillna(self.value) return df_out class HoleGeneratorTest(_HoleGenerator): + """Group tests for HoleGenerator.""" + def __init__(self, mask: pd.Series, subset: Optional[List[str]] = None): + """Init HoleGenerator.""" super().__init__(n_splits=1, subset=subset) self.mask = mask def generate_mask(self, X: pd.DataFrame) -> pd.DataFrame: + """Generate mask.""" df_out = X.copy() for col in df_out: df_out[col] = self.mask @@ -69,19 +86,27 @@ def generate_mask(self, X: pd.DataFrame) -> pd.DataFrame: def test_hyperparameters_get_objective() -> None: + """Test get_objective.""" imputer = ImputerTest() - generator = HoleGeneratorTest(pd.Series([False, False, True, True]), subset=["some_col"]) + generator = HoleGeneratorTest( + pd.Series([False, False, True, True]), subset=["some_col"] + ) metric = "mse" names_hyperparams = ["value"] df = pd.DataFrame({"some_col": [np.nan, 0, 3, 5]}) - fun_obj = hyperparameters.get_objective(imputer, df, generator, metric, names_hyperparams) + fun_obj = hyperparameters.get_objective( + imputer, df, generator, metric, names_hyperparams + ) assert fun_obj([4]) == 1 assert fun_obj([0]) == (3**2 + 5**2) / 2 def test_hyperparameters_optimize(): + """Test optimize.""" imputer = ImputerTest() - generator = HoleGeneratorTest(pd.Series([False, False, True, True]), subset=["some_col"]) + generator = HoleGeneratorTest( + pd.Series([False, False, True, True]), subset=["some_col"] + ) metric = "mse" dict_config_opti = {"value": ho.hp.uniform("value", 0, 10)} df = pd.DataFrame({"some_col": [np.nan, 0, 3, 5]}) diff --git a/tests/benchmark/test_metrics.py b/tests/benchmark/test_metrics.py index 0c768054..26fa0f7c 100644 --- a/tests/benchmark/test_metrics.py +++ b/tests/benchmark/test_metrics.py @@ -2,12 +2,11 @@ # # Evaluation metrics # # ###################### -from math import exp import numpy as np -from numpy import random as npr import pandas as pd import pytest import scipy +from numpy import random as npr from qolmat.benchmark import metrics from qolmat.utils.exceptions import NotEnoughSamples @@ -16,9 +15,13 @@ {"col1": [0, np.nan, 2, 3, np.nan], "col2": [-1, np.nan, 0.5, 1, 1.5]} ) -df_complete = pd.DataFrame({"col1": [0, 2, 2, 3, 4], "col2": [-1, -2, 0.5, 1, 1.5]}) +df_complete = pd.DataFrame( + {"col1": [0, 2, 2, 3, 4], "col2": [-1, -2, 0.5, 1, 1.5]} +) -df_imputed = pd.DataFrame({"col1": [0, 1, 2, 3.5, 4], "col2": [-1.5, 0, 1.5, 2, 1.5]}) +df_imputed = pd.DataFrame( + {"col1": [0, 1, 2, 3.5, 4], "col2": [-1.5, 0, 1.5, 2, 1.5]} +) df_mask = pd.DataFrame( { @@ -31,7 +34,9 @@ @pytest.mark.parametrize("df1", [df_incomplete]) @pytest.mark.parametrize("df2", [df_imputed]) @pytest.mark.parametrize("df_mask", [df_mask]) -def test_mean_squared_error(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> None: +def test_mean_squared_error( + df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame +) -> None: assert metrics.mean_squared_error(df1, df1, df_mask).equals( pd.Series([0.0, 0.0], index=["col1", "col2"]) ) @@ -59,7 +64,9 @@ def test_root_mean_squared_error( @pytest.mark.parametrize("df1", [df_incomplete]) @pytest.mark.parametrize("df2", [df_imputed]) @pytest.mark.parametrize("df_mask", [df_mask]) -def test_mean_absolute_error(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> None: +def test_mean_absolute_error( + df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame +) -> None: assert metrics.mean_absolute_error(df1, df1, df_mask).equals( pd.Series([0.0, 0.0], index=["col1", "col2"]) ) @@ -90,9 +97,9 @@ def test_mean_absolute_percentage_error( def test_weighted_mean_absolute_percentage_error( df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame ) -> None: - assert metrics.weighted_mean_absolute_percentage_error(df1, df1, df_mask).equals( - pd.Series([0.0, 0.0], index=["col1", "col2"]) - ) + assert metrics.weighted_mean_absolute_percentage_error( + df1, df1, df_mask + ).equals(pd.Series([0.0, 0.0], index=["col1", "col2"])) result = metrics.weighted_mean_absolute_percentage_error(df1, df2, df_mask) expected = pd.Series([0.1, 1.0], index=["col1", "col2"]) np.testing.assert_allclose(result, expected, atol=1e-3) @@ -101,7 +108,9 @@ def test_weighted_mean_absolute_percentage_error( @pytest.mark.parametrize("df1", [df_incomplete]) @pytest.mark.parametrize("df2", [df_imputed]) @pytest.mark.parametrize("df_mask", [df_mask]) -def test_accuracy(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> None: +def test_accuracy( + df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame +) -> None: result = metrics.accuracy(df1, df1, df_mask) expected = pd.Series([1.0, 1.0], index=["col1", "col2"]) pd.testing.assert_series_equal(result, expected) @@ -113,17 +122,23 @@ def test_accuracy(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) - @pytest.mark.parametrize("df1", [df_incomplete]) @pytest.mark.parametrize("df2", [df_imputed]) @pytest.mark.parametrize("df_mask", [df_mask]) -def test_wasserstein_distance(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> None: +def test_wasserstein_distance( + df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame +) -> None: dist = metrics.dist_wasserstein(df1, df1, df_mask, method="columnwise") assert dist.equals(pd.Series([0.0, 0.0], index=["col1", "col2"])) dist = metrics.dist_wasserstein(df1, df2, df_mask, method="columnwise") - assert dist.round(3).equals(pd.Series([0.250, 0.833], index=["col1", "col2"])) + assert dist.round(3).equals( + pd.Series([0.250, 0.833], index=["col1", "col2"]) + ) @pytest.mark.parametrize("df1", [df_incomplete]) @pytest.mark.parametrize("df2", [df_imputed]) @pytest.mark.parametrize("df_mask", [df_mask]) -def test_kl_divergence(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> None: +def test_kl_divergence( + df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame +) -> None: result = metrics.kl_divergence(df1, df1, df_mask, method="columnwise") expected = pd.Series([0.0, 0.0], index=["col1", "col2"]) pd.testing.assert_series_equal(result, expected, atol=1e-3) @@ -133,7 +148,9 @@ def test_kl_divergence(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFra pd.testing.assert_series_equal(result, expected, atol=1e-3) df_nonan = df1.notna() - result = metrics.kl_divergence(df1, df2, df_nonan, method="gaussian", min_n_rows=2) + result = metrics.kl_divergence( + df1, df2, df_nonan, method="gaussian", min_n_rows=2 + ) expected = pd.Series([1.029], index=["All"]) pd.testing.assert_series_equal(result, expected, atol=1e-3) @@ -190,26 +207,38 @@ def test_sum_pairwise_distances( @pytest.mark.parametrize("df1", [df_incomplete]) @pytest.mark.parametrize("df2", [df_imputed]) @pytest.mark.parametrize("df_mask", [df_mask]) -def test_sum_energy_distances(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame) -> None: +def test_sum_energy_distances( + df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame +) -> None: sum_distances_df1 = np.sum( scipy.spatial.distance.cdist( - df1[df_mask].fillna(0.0), df1[df_mask].fillna(0.0), metric="cityblock" + df1[df_mask].fillna(0.0), + df1[df_mask].fillna(0.0), + metric="cityblock", ) ) sum_distances_df2 = np.sum( scipy.spatial.distance.cdist( - df2[df_mask].fillna(0.0), df2[df_mask].fillna(0.0), metric="cityblock" + df2[df_mask].fillna(0.0), + df2[df_mask].fillna(0.0), + metric="cityblock", ) ) sum_distances_df1_df2 = np.sum( scipy.spatial.distance.cdist( - df1[df_mask].fillna(0.0), df2[df_mask].fillna(0.0), metric="cityblock" + df1[df_mask].fillna(0.0), + df2[df_mask].fillna(0.0), + metric="cityblock", ) ) - energy_distance_scipy = 2 * sum_distances_df1_df2 - sum_distances_df1 - sum_distances_df2 + energy_distance_scipy = ( + 2 * sum_distances_df1_df2 - sum_distances_df1 - sum_distances_df2 + ) energy_distance_qolmat = metrics.sum_energy_distances(df1, df2, df_mask) - assert energy_distance_qolmat.equals(pd.Series(energy_distance_scipy, index=["All"])) + assert energy_distance_qolmat.equals( + pd.Series(energy_distance_scipy, index=["All"]) + ) @pytest.mark.parametrize("df1", [df_incomplete]) @@ -218,20 +247,23 @@ def test_sum_energy_distances(df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd. def test_mean_difference_correlation_matrix_numerical_features( df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame ) -> None: - assert metrics.mean_difference_correlation_matrix_numerical_features(df1, df1, df_mask).equals( - pd.Series([0.0, 0.0], index=["col1", "col2"]) - ) + assert metrics.mean_difference_correlation_matrix_numerical_features( + df1, df1, df_mask + ).equals(pd.Series([0.0, 0.0], index=["col1", "col2"])) assert metrics.mean_difference_correlation_matrix_numerical_features( df1, df1, df_mask, False ).equals(pd.Series([0.0, 0.0], index=["col1", "col2"])) - assert metrics.mean_difference_correlation_matrix_numerical_features(df1, df2, df_mask).equals( - pd.Series([0.0, 0.0], index=["col1", "col2"]) - ) + assert metrics.mean_difference_correlation_matrix_numerical_features( + df1, df2, df_mask + ).equals(pd.Series([0.0, 0.0], index=["col1", "col2"])) df_incomplete_cat = pd.DataFrame( - {"col1": ["a", np.nan, "a", "b", np.nan], "col2": ["c", np.nan, "d", "b", "d"]} + { + "col1": ["a", np.nan, "a", "b", np.nan], + "col2": ["c", np.nan, "d", "b", "d"], + } ) df_imputed_cat = pd.DataFrame( @@ -279,7 +311,10 @@ def test_mean_difference_correlation_matrix_categorical_features( df_incomplete_cat_num = pd.DataFrame( - {"col1": ["a", np.nan, "a", "b", np.nan], "col2": [-1, np.nan, 0.5, 1, 1.5]} + { + "col1": ["a", np.nan, "a", "b", np.nan], + "col2": [-1, np.nan, 0.5, 1, 1.5], + } ) df_imputed_cat_num = pd.DataFrame( @@ -287,7 +322,10 @@ def test_mean_difference_correlation_matrix_categorical_features( ) df_mask_cat_num = pd.DataFrame( - {"col1": [True, False, True, True, False], "col2": [True, False, True, True, False]} + { + "col1": [True, False, True, True, False], + "col2": [True, False, True, True, False], + } ) @@ -318,7 +356,9 @@ def test_exception_raise_different_shapes( df1: pd.DataFrame, df2: pd.DataFrame, df_mask: pd.DataFrame ) -> None: with pytest.raises(Exception): - metrics.mean_difference_correlation_matrix_numerical_features(df1, df2, df_mask) + metrics.mean_difference_correlation_matrix_numerical_features( + df1, df2, df_mask + ) with pytest.raises(Exception): metrics.frechet_distance_base(df1, df2) @@ -332,7 +372,9 @@ def test_exception_raise_no_numerical_column_found( with pytest.raises(Exception): metrics.kolmogorov_smirnov_test(df1, df2, df_mask) with pytest.raises(Exception): - metrics.mean_difference_correlation_matrix_numerical_features(df1, df2, df_mask) + metrics.mean_difference_correlation_matrix_numerical_features( + df1, df2, df_mask + ) @pytest.mark.parametrize("df1", [df_incomplete]) @@ -346,7 +388,10 @@ def test_exception_raise_no_categorical_column_found( df_incomplete_cat_num_bad = pd.DataFrame( - {"col1": ["a", np.nan, "c", "b", np.nan], "col2": [-1, np.nan, 0.5, 0.5, 1.5]} + { + "col1": ["a", np.nan, "c", "b", np.nan], + "col2": [-1, np.nan, 0.5, 0.5, 1.5], + } ) @@ -376,14 +421,19 @@ def test_pattern_based_weighted_mean_metric( rng = npr.default_rng(123) -df_gauss1 = pd.DataFrame(rng.multivariate_normal([0, 0], [[1, 0.2], [0.2, 2]], size=100)) -df_gauss2 = pd.DataFrame(rng.multivariate_normal([0, 1], [[1, 0.2], [0.2, 2]], size=100)) +df_gauss1 = pd.DataFrame( + rng.multivariate_normal([0, 0], [[1, 0.2], [0.2, 2]], size=100) +) +df_gauss2 = pd.DataFrame( + rng.multivariate_normal([0, 1], [[1, 0.2], [0.2, 2]], size=100) +) df_mask_gauss = pd.DataFrame(np.full_like(df_gauss1, True)) def test_pattern_mae_comparison(mocker) -> None: - - mock_metric = mocker.patch("qolmat.benchmark.metrics.accuracy_1D", return_value=0) + mock_metric = mocker.patch( + "qolmat.benchmark.metrics.accuracy_1D", return_value=0 + ) df_nonan = df_incomplete.notna() metrics.pattern_based_weighted_mean_metric( diff --git a/tests/benchmark/test_missing_patterns.py b/tests/benchmark/test_missing_patterns.py index 0fa06e69..4cd29455 100644 --- a/tests/benchmark/test_missing_patterns.py +++ b/tests/benchmark/test_missing_patterns.py @@ -4,7 +4,9 @@ from qolmat.benchmark import missing_patterns as mp -df_complet = pd.DataFrame({"col1": [i for i in range(100)], "col2": [2 * i for i in range(100)]}) +df_complet = pd.DataFrame( + {"col1": list(range(100)), "col2": [2 * i for i in range(100)]} +) df_incomplet = df_complet.copy() df_incomplet.iloc[99, :] = np.nan @@ -20,9 +22,15 @@ df_incomplet_group.index = df_incomplet_group.index.set_names("group") list_generators = { - "geo": mp.GeometricHoleGenerator(n_splits=2, ratio_masked=0.1, random_state=42), - "unif": mp.UniformHoleGenerator(n_splits=2, ratio_masked=0.1, random_state=42), - "multi": mp.MultiMarkovHoleGenerator(n_splits=2, ratio_masked=0.1, random_state=42), + "geo": mp.GeometricHoleGenerator( + n_splits=2, ratio_masked=0.1, random_state=42 + ), + "unif": mp.UniformHoleGenerator( + n_splits=2, ratio_masked=0.1, random_state=42 + ), + "multi": mp.MultiMarkovHoleGenerator( + n_splits=2, ratio_masked=0.1, random_state=42 + ), "group": mp.GroupedHoleGenerator( n_splits=2, ratio_masked=0.1, random_state=42, groups=("group",) ), @@ -38,7 +46,9 @@ (df_incomplet_group, list_generators["group"]), ], ) -def test_SamplerHoleGenerator_split(df: pd.DataFrame, generator: mp._HoleGenerator) -> None: +def test_SamplerHoleGenerator_split( + df: pd.DataFrame, generator: mp._HoleGenerator +) -> None: mask = generator.split(df)[0] col1_holes = mask["col1"].sum() col2_holes = mask["col2"].sum() @@ -57,7 +67,9 @@ def test_SamplerHoleGenerator_split(df: pd.DataFrame, generator: mp._HoleGenerat (df_incomplet_group, list_generators["group"]), ], ) -def test_SamplerHoleGenerator_reproducible(df: pd.DataFrame, generator: mp._HoleGenerator) -> None: +def test_SamplerHoleGenerator_reproducible( + df: pd.DataFrame, generator: mp._HoleGenerator +) -> None: generator.random_state = 42 mask1 = generator.split(df)[0] generator.random_state = 43 @@ -81,7 +93,9 @@ def test_SamplerHoleGenerator_reproducible(df: pd.DataFrame, generator: mp._Hole def test_SamplerHoleGenerator_without_real_nans( df: pd.DataFrame, generator: mp._HoleGenerator ) -> None: - real_nan = np.random.choice([True, False], size=df.size, p=[0.4, 0.6]).reshape(100, 2) + real_nan = np.random.choice( + [True, False], size=df.size, p=[0.4, 0.6] + ).reshape(100, 2) df[real_nan] = np.nan mask = generator.split(df)[0] @@ -92,5 +106,9 @@ def test_SamplerHoleGenerator_without_real_nans( loc_real_nans_col2 = np.where(df["col2"].isna())[0] loc_mask_col2 = np.where(mask["col2"])[0] - np.testing.assert_allclose(len(set(loc_real_nans_col1) & set(loc_mask_col1)), 0) - np.testing.assert_allclose(len(set(loc_real_nans_col2) & set(loc_mask_col2)), 0) + np.testing.assert_allclose( + len(set(loc_real_nans_col1) & set(loc_mask_col1)), 0 + ) + np.testing.assert_allclose( + len(set(loc_real_nans_col2) & set(loc_mask_col2)), 0 + ) diff --git a/tests/imputations/rpca/test_rpca.py b/tests/imputations/rpca/test_rpca.py index 1430dc4e..34672ef2 100644 --- a/tests/imputations/rpca/test_rpca.py +++ b/tests/imputations/rpca/test_rpca.py @@ -1,32 +1,21 @@ from typing import Tuple + import numpy as np -import pandas as pd -import pytest from numpy.typing import NDArray -from pytest_mock.plugin import MockerFixture -from qolmat.imputations.rpca.rpca import RPCA - -# X_incomplete = np.array([[1, np.nan], [4, 2], [np.nan, 4]]) - -# X_exp_nrows_1_prepare_data = np.array([1.0, np.nan, 4.0, 2.0, np.nan, 4.0]) -# X_exp_nrows_6_prepare_data = np.concatenate( -# [X_incomplete.reshape(-1, 6).flatten(), np.ones((1, 94)).flatten() * np.nan] -# ) - -# period = 100 -# max_iter = 256 -# mu = 0.5 -# tau = 0.5 -# lam = 1 +from qolmat.imputations.rpca.rpca import RPCA class RPCAMock(RPCA): + """Mock for RPCA.""" + def __init__(self): + """Mock for init RPCA.""" super().__init__() self.Q = None def decompose(self, D: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]: + """Mock for decompose function.""" self.call_count = 1 return D, D diff --git a/tests/imputations/rpca/test_rpca_noisy.py b/tests/imputations/rpca/test_rpca_noisy.py index 78f62e41..d20aeaba 100644 --- a/tests/imputations/rpca/test_rpca_noisy.py +++ b/tests/imputations/rpca/test_rpca_noisy.py @@ -4,7 +4,6 @@ import pytest from numpy.typing import NDArray -from qolmat.imputations.rpca import rpca_utils from qolmat.imputations.rpca.rpca_noisy import RpcaNoisy from qolmat.utils import utils from qolmat.utils.data import generate_artificial_ts @@ -57,7 +56,9 @@ def test_check_cost_function_minimized_warning( ): """Test warning when the cost function is not minimized.""" with pytest.warns(UserWarning): - RpcaNoisy()._check_cost_function_minimized(obs, lr, ano, omega, lam, tau) + RpcaNoisy()._check_cost_function_minimized( + obs, lr, ano, omega, lam, tau + ) @pytest.mark.parametrize( @@ -85,7 +86,9 @@ def test_check_cost_function_minimized_no_warning( ): """Test no warning when the cost function is minimized.""" with warnings.catch_warnings(record=True) as record: - RpcaNoisy()._check_cost_function_minimized(obs, lr, ano, omega, lam, tau) + RpcaNoisy()._check_cost_function_minimized( + obs, lr, ano, omega, lam, tau + ) assert len(record) == 0 @@ -108,7 +111,9 @@ def test_rpca_decompose_rpca_shape(norm: str): rank = 2 rpca = RpcaNoisy(rank=rank, norm=norm) Omega = ~np.isnan(X_test) - M_result, A_result, L_result, Q_result = rpca.decompose_with_basis(X_test, Omega) + M_result, A_result, L_result, Q_result = rpca.decompose_with_basis( + X_test, Omega + ) n_rows, n_cols = X_test.shape assert M_result.shape == (n_rows, n_cols) assert A_result.shape == (n_rows, n_cols) @@ -143,7 +148,9 @@ def test_rpca_noisy_zero_tau(X: NDArray, lam: float, X_interpolated: NDArray): "X, tau, X_interpolated", [(X_incomplete, 0.4, X_interpolated), (X_incomplete, 2.4, X_interpolated)], ) -def test_rpca_noisy_zero_lambda(X: NDArray, tau: float, X_interpolated: NDArray): +def test_rpca_noisy_zero_lambda( + X: NDArray, tau: float, X_interpolated: NDArray +): """Test RPCA noisy results if lambda equals zero.""" rpca = RpcaNoisy(tau=tau, lam=0, norm="L2") Omega = ~np.isnan(X) @@ -154,7 +161,9 @@ def test_rpca_noisy_zero_lambda(X: NDArray, tau: float, X_interpolated: NDArray) def test_rpca_noisy_decompose_rpca(synthetic_temporal_data): """Test RPCA noisy results for time series data. - Check if the cost function is smaller at the end than at the start.""" + + Check if the cost function is smaller at the end than at the start. + """ signal = synthetic_temporal_data period = 100 tau = 1 @@ -166,24 +175,27 @@ def test_rpca_noisy_decompose_rpca(synthetic_temporal_data): low_rank_init = D anomalies_init = np.zeros(D.shape) - cost_init = RpcaNoisy.cost_function(D, low_rank_init, anomalies_init, Omega, tau, lam) + cost_init = RpcaNoisy.cost_function( + D, low_rank_init, anomalies_init, Omega, tau, lam + ) - X_result, A_result, _, _ = RpcaNoisy.minimise_loss(D, Omega, rank, tau, lam) - cost_result = RpcaNoisy.cost_function(D, X_result, A_result, Omega, tau, lam) + X_result, A_result, _, _ = RpcaNoisy.minimise_loss( + D, Omega, rank, tau, lam + ) + cost_result = RpcaNoisy.cost_function( + D, X_result, A_result, Omega, tau, lam + ) assert cost_result <= cost_init - # assert np.linalg.norm(X_input_rpca, "nuc") >= 1 / 2 * np.linalg.norm( - # X_input_rpca - X_result.reshape(period, -1) - A_result.reshape(period, -1), - # "fro", - # ) ** 2 + tau * np.linalg.norm(X_result.reshape(period, -1), "nuc") + lam * np.sum( - # np.abs(A_result.reshape(period, -1)) - # ) +def test_rpca_noisy_temporal_signal_temporal_regularisations( + synthetic_temporal_data, +): + """Test RPCA noisy results for TS data with temporal regularisations. -def test_rpca_noisy_temporal_signal_temporal_regularisations(synthetic_temporal_data): - """Test RPCA noisy results for time series data with temporal regularisations. - Check if the cost function is smaller at the end than at the start.""" + Check if the cost function is smaller at the end than at the start. + """ signal = synthetic_temporal_data period = 10 tau = 1 diff --git a/tests/imputations/rpca/test_rpca_pcp.py b/tests/imputations/rpca/test_rpca_pcp.py index c7ab69e5..de997d90 100644 --- a/tests/imputations/rpca/test_rpca_pcp.py +++ b/tests/imputations/rpca/test_rpca_pcp.py @@ -85,6 +85,7 @@ def test_rpca_rpca_pcp_get_params_scale(X: NDArray): @pytest.mark.parametrize("X, mu", [(X_complete, small_mu)]) def test_rpca_rpca_pcp_zero_lambda_small_mu(X: NDArray, mu: float): """Test RPCA PCP results if lambda equals zero. + The problem is ill-conditioned and the result depends on the parameter mu; case when mu is small. """ @@ -98,6 +99,7 @@ def test_rpca_rpca_pcp_zero_lambda_small_mu(X: NDArray, mu: float): @pytest.mark.parametrize("X, mu", [(X_complete, large_mu)]) def test_rpca_rpca_pcp_zero_lambda_large_mu(X: NDArray, mu: float): """Test RPCA PCP results if lambda equals zero. + The problem is ill-conditioned and the result depends on the parameter mu; case when mu is large. """ @@ -120,7 +122,9 @@ def test_rpca_rpca_pcp_large_lambda_small_mu(X: NDArray, mu: float): def test_rpca_temporal_signal(synthetic_temporal_data): """Test RPCA PCP results for time series data. - Check if the cost function is smaller at the end than at the start.""" + + Check if the cost function is smaller at the end than at the start. + """ signal = synthetic_temporal_data period = 100 lam = 0.1 @@ -130,6 +134,6 @@ def test_rpca_temporal_signal(synthetic_temporal_data): Omega = ~np.isnan(D) D_interpolated = utils.linear_interpolation(D) X_result, A_result = rpca.decompose(D, Omega) - assert np.linalg.norm(D_interpolated, "nuc") >= np.linalg.norm(X_result, "nuc") + lam * np.sum( - np.abs(A_result) - ) + assert np.linalg.norm(D_interpolated, "nuc") >= np.linalg.norm( + X_result, "nuc" + ) + lam * np.sum(np.abs(A_result)) diff --git a/tests/imputations/rpca/test_rpca_utils.py b/tests/imputations/rpca/test_rpca_utils.py index 775c9d98..120bff83 100644 --- a/tests/imputations/rpca/test_rpca_utils.py +++ b/tests/imputations/rpca/test_rpca_utils.py @@ -1,14 +1,14 @@ import numpy as np -from numpy.typing import NDArray import pytest +from numpy.typing import NDArray + from qolmat.imputations.rpca.rpca_utils import ( approx_rank, + l1_norm, soft_thresholding, svd_thresholding, - l1_norm, toeplitz_matrix, ) -from qolmat.utils.utils import fold_signal X_incomplete = np.array( [ @@ -20,7 +20,9 @@ ] ) -X_complete = np.array([[1, 7, 4, 4], [5, 2, 4, 4], [-3, 3, 3, 3], [2, -1, 5, 5], [2, 1, 5, 5]]) +X_complete = np.array( + [[1, 7, 4, 4], [5, 2, 4, 4], [-3, 3, 3, 3], [2, -1, 5, 5], [2, 1, 5, 5]] +) @pytest.mark.parametrize("X", [X_complete]) diff --git a/tests/imputations/test_em_sampler.py b/tests/imputations/test_em_sampler.py index 832737dc..21e2ffd0 100644 --- a/tests/imputations/test_em_sampler.py +++ b/tests/imputations/test_em_sampler.py @@ -1,22 +1,29 @@ from typing import List, Literal + import numpy as np import pytest +import scipy from numpy.typing import NDArray from scipy import linalg -import scipy from sklearn.datasets import make_spd_matrix -from qolmat.utils import utils - from qolmat.imputations import em_sampler -from qolmat.utils.exceptions import IllConditioned +from qolmat.utils import utils np.random.seed(42) A: NDArray = np.array([[3, 1, 0], [1, 1, 0], [0, 0, 1]], dtype=float) -A_inverse: NDArray = np.array([[0.5, -0.5, 0], [-0.5, 1.5, 0], [0, 0, 1]], dtype=float) +A_inverse: NDArray = np.array( + [[0.5, -0.5, 0], [-0.5, 1.5, 0], [0, 0, 1]], dtype=float +) X_missing = np.array( - [[1, np.nan, 1], [2, np.nan, 3], [1, 4, np.nan], [-1, 2, 1], [1, 1, np.nan]], + [ + [1, np.nan, 1], + [2, np.nan, 3], + [1, 4, np.nan], + [-1, 2, 1], + [1, 1, np.nan], + ], dtype=float, ) mask: NDArray = np.isnan(X_missing) @@ -40,7 +47,6 @@ def generate_multinormal_predefined_mean_cov(d=3, n=500): mask[ind, j] = True X_missing = X.copy() X_missing[mask] = np.nan - # return {"mean": mean, "covariance": covariance, "X": X, "X_missing": X_missing} return X, X_missing, mean, covariance @@ -93,16 +99,20 @@ def test_gradient_conjugue( """Test the conjugate gradient algorithm.""" X_first_guess = utils.impute_nans(X_missing) X_result = em_sampler._conjugate_gradient(A, X_first_guess, mask) - X_expected = np.array([[1, -1, 1], [2, -2, 3], [1, 4, 0], [-1, 2, 1], [1, 1, 0]], dtype=float) + X_expected = np.array( + [[1, -1, 1], [2, -2, 3], [1, 4, 0], [-1, 2, 1], [1, 1, 0]], dtype=float + ) - assert np.sum(X_result * (X_result @ A)) <= np.sum(X_first_guess * (X_first_guess @ A)) + assert np.sum(X_result * (X_result @ A)) <= np.sum( + X_first_guess * (X_first_guess @ A) + ) assert np.allclose(X_missing[~mask], X_result[~mask]) assert ((X_result @ A)[mask] == 0).all() np.testing.assert_allclose(X_result, X_expected, atol=1e-5) def test_get_lag_p(): - """Test if it can retrieve the lag p""" + """Test if it can retrieve the lag p.""" X, _, _, _ = generate_varp_process(d=3, n=1000, p=2) varpem = em_sampler.VARpEM() varpem.fit(X) @@ -120,7 +130,8 @@ def test_fit_calls(mocker, X_missing: NDArray) -> None: """Test number of calls of some methods in MultiNormalEM.""" max_iter_em = 3 mock_sample_ou = mocker.patch( - "qolmat.imputations.em_sampler.MultiNormalEM._sample_ou", return_value=X_missing + "qolmat.imputations.em_sampler.MultiNormalEM._sample_ou", + return_value=X_missing, ) mock_maximize_likelihood = mocker.patch( "qolmat.imputations.em_sampler.MultiNormalEM._maximize_likelihood", @@ -152,7 +163,11 @@ def test_fit_calls(mocker, X_missing: NDArray) -> None: @pytest.mark.parametrize( "means, covs, logliks", [ - ([np.array([1, 2, 3, 3])] * 15, [np.array([1, 2, 3, 3])] * 15, [1] * 15), + ( + [np.array([1, 2, 3, 3])] * 15, + [np.array([1, 2, 3, 3])] * 15, + [1] * 15, + ), ( [np.array([1, 2, 3, 3])] * 15, [np.random.uniform(low=0, high=100, size=(1, 4))[0]] * 15, @@ -180,7 +195,7 @@ def test_em_sampler_check_convergence_true( em.dict_criteria_stop["means"] = means em.dict_criteria_stop["covs"] = covs em.dict_criteria_stop["logliks"] = logliks - assert em._check_convergence() == True + assert em._check_convergence() @pytest.mark.parametrize( @@ -197,7 +212,7 @@ def test_em_sampler_check_convergence_false( em.dict_criteria_stop["means"] = means em.dict_criteria_stop["covs"] = covs em.dict_criteria_stop["logliks"] = logliks - assert em._check_convergence() == True + assert em._check_convergence() @pytest.mark.parametrize( @@ -231,7 +246,9 @@ def test_sample_ou_2d(model): assert abs(mean_est - mean_theo) < np.sqrt(var_theo / n_samples) * q_alpha ratio_inf = scipy.stats.chi2.ppf(alpha / 2, n_samples) / (n_samples - 1) - ratio_sup = scipy.stats.chi2.ppf(1 - alpha / 2, n_samples) / (n_samples - 1) + ratio_sup = scipy.stats.chi2.ppf(1 - alpha / 2, n_samples) / ( + n_samples - 1 + ) ratio = var_est / var_theo @@ -261,7 +278,7 @@ def test_varem_sampler_check_convergence_true( em.dict_criteria_stop["B"] = list_B em.dict_criteria_stop["S"] = list_S em.dict_criteria_stop["logliks"] = logliks - assert em._check_convergence() == True + assert em._check_convergence() @pytest.mark.parametrize( @@ -278,12 +295,14 @@ def test_varem_sampler_check_convergence_false( em.dict_criteria_stop["B"] = list_B em.dict_criteria_stop["S"] = list_S em.dict_criteria_stop["logliks"] = logliks - assert em._check_convergence() == True + assert em._check_convergence() def test_illconditioned_multinormalem() -> None: """Test that data with colinearity raises an exception.""" - X = np.array([[1, np.nan, 8, 1], [3, 1, 4, 2], [2, 3, np.nan, 1]], dtype=float) + X = np.array( + [[1, np.nan, 8, 1], [3, 1, 4, 2], [2, 3, np.nan, 1]], dtype=float + ) model = em_sampler.MultiNormalEM() with pytest.warns(UserWarning): _ = model.fit_transform(X) @@ -293,7 +312,7 @@ def test_illconditioned_multinormalem() -> None: def test_no_more_nan_multinormalem() -> None: - """Test there are no more missing values after the MultiNormalEM algorithm.""" + """Test there are no more missing values after the MultiNormalEM algo.""" X = np.array([[1, np.nan], [3, 1], [np.nan, 3]], dtype=float) model = em_sampler.MultiNormalEM() X_imp = model.fit_transform(X) @@ -310,9 +329,11 @@ def test_no_more_nan_varpem() -> None: assert np.sum(np.isnan(X_imputed)) == 0 -def test_fit_parameters_multinormalem(): - """Test the fit MultiNormalEM provides good parameters estimates (no imputation).""" - X, X_missing, mean, covariance = generate_multinormal_predefined_mean_cov(d=2, n=10000) +def test_fit_parameters_multinormalem_no_imputation(): + """Test fit MultiNormalEM provides good parameters estimates.""" + X, X_missing, mean, covariance = generate_multinormal_predefined_mean_cov( + d=2, n=10000 + ) em = em_sampler.MultiNormalEM() em.fit_parameters(X) np.testing.assert_allclose(em.means, mean, atol=1e-1) @@ -320,8 +341,10 @@ def test_fit_parameters_multinormalem(): def test_mean_covariance_multinormalem(): - """Test the MultiNormalEM provides good mean and covariance estimations.""" - X, X_missing, mean, covariance = generate_multinormal_predefined_mean_cov(d=2, n=1000) + """Test MultiNormalEM provides good mean and covariance estimations.""" + X, X_missing, mean, covariance = generate_multinormal_predefined_mean_cov( + d=2, n=1000 + ) em = em_sampler.MultiNormalEM() X_imputed = em.fit_transform(X_missing) @@ -333,11 +356,14 @@ def test_mean_covariance_multinormalem(): np.testing.assert_allclose(em.means, mean, rtol=1e-1, atol=1e-1) np.testing.assert_allclose(em.cov, covariance, rtol=1e-1, atol=1e-1) np.testing.assert_allclose(mean_imputed, mean, rtol=1e-1, atol=1e-1) - np.testing.assert_allclose(covariance_imputed, covariance, rtol=1e-1, atol=1e-1) + np.testing.assert_allclose( + covariance_imputed, covariance, rtol=1e-1, atol=1e-1 + ) def test_multinormal_em_minimize_llik(): - X, X_missing, mean, covariance = generate_multinormal_predefined_mean_cov(d=2, n=1000) + """Test that the loglikelihood of the imputed data is lower.""" + X, X_missing, _, _ = generate_multinormal_predefined_mean_cov(d=2, n=1000) imputer = em_sampler.MultiNormalEM(method="mle", random_state=11) X_imputed = imputer.fit_transform(X_missing) llikelihood_imputed = imputer.get_loglikelihood(X_imputed) @@ -354,6 +380,7 @@ def test_multinormal_em_minimize_llik(): @pytest.mark.parametrize("method", ["sample", "mle"]) def test_multinormal_em_fit_transform(method: Literal["mle", "sample"]): + """Test fit_transform method returns the same result as the fit method.""" imputer = em_sampler.MultiNormalEM(method=method, random_state=11) X = X_missing.copy() result = imputer.fit_transform(X) @@ -390,7 +417,9 @@ def test_parameters_after_imputation_varpem(p: int): def test_varpem_fit_transform(): imputer = em_sampler.VARpEM(method="mle", random_state=11) - X = np.array([[1, 1, 1, 1], [np.nan, np.nan, 3, 2], [1, 2, 2, 1], [2, 2, 2, 2]]) + X = np.array( + [[1, 1, 1, 1], [np.nan, np.nan, 3, 2], [1, 2, 2, 1], [2, 2, 2, 2]] + ) result = imputer.fit_transform(X) assert result.shape == X.shape np.testing.assert_allclose(result[~np.isnan(X)], X[~np.isnan(X)]) @@ -439,12 +468,6 @@ def test_pretreatment_temporal(em): np.testing.assert_allclose(mask_result, mask_expected) -# X_missing = np.array( -# [[1, np.nan, 1], [2, np.nan, 3], [1, 4, np.nan], [-1, 2, 1], [1, 1, np.nan]], -# dtype=float, -# ) - - @pytest.mark.parametrize( "em", [ diff --git a/tests/imputations/test_imputers.py b/tests/imputations/test_imputers.py index bea18d9a..5069f0bd 100644 --- a/tests/imputations/test_imputers.py +++ b/tests/imputations/test_imputers.py @@ -5,26 +5,33 @@ import pytest from sklearn.ensemble import ExtraTreesRegressor from sklearn.linear_model import LinearRegression -from sklearn.utils.estimator_checks import check_estimator, parametrize_with_checks -from qolmat.benchmark.hyperparameters import HyperValue +from sklearn.utils.estimator_checks import ( + parametrize_with_checks, +) +from qolmat.benchmark.hyperparameters import HyperValue from qolmat.imputations import imputers -df_complete = pd.DataFrame({"col1": [0, 1, 2, 3, 4], "col2": [-1, 0, 0.5, 1, 1.5]}) +df_complete = pd.DataFrame( + {"col1": [0, 1, 2, 3, 4], "col2": [-1, 0, 0.5, 1, 1.5]} +) df_incomplete = pd.DataFrame( {"col1": [0, np.nan, 2, 3, np.nan], "col2": [-1, np.nan, 0.5, np.nan, 1.5]} ) df_mixed = pd.DataFrame( - {"col1": [0, np.nan, 2, 3, np.nan], "col2": ["a", np.nan, "b", np.nan, "b"]} + { + "col1": [0, np.nan, 2, 3, np.nan], + "col2": ["a", np.nan, "b", np.nan, "b"], + } ) df_timeseries = pd.DataFrame( pd.DataFrame( { - "col1": [i for i in range(20)], - "col2": [0, np.nan, 2, np.nan, 2] + [i for i in range(5, 20)], + "col1": list(range(20)), + "col2": [0, np.nan, 2, np.nan, 2] + list(range(5, 20)), }, index=pd.date_range("2023-04-17", periods=20, freq="D"), ) @@ -80,14 +87,18 @@ def test_hyperparameters_get_hyperparameters() -> None: } -@pytest.mark.parametrize("col, expected", [("col1", expected1), ("col2", expected2)]) +@pytest.mark.parametrize( + "col, expected", [("col1", expected1), ("col2", expected2)] +) def test_hyperparameters_get_hyperparameters_modified( col: str, expected: Dict[str, HyperValue] ) -> None: imputer = imputers.ImputerRpcaNoisy() for key, val in hyperparams_global.items(): setattr(imputer, key, val) - imputer.imputer_params = tuple(set(imputer.imputer_params) | set(hyperparams_global.keys())) + imputer.imputer_params = tuple( + set(imputer.imputer_params) | set(hyperparams_global.keys()) + ) hyperparams = imputer.get_hyperparams(col) assert hyperparams == expected @@ -105,7 +116,9 @@ def test_hyperparameters_get_hyperparameters_modified( @pytest.mark.parametrize( "df", [pd.DataFrame({"col1": [np.nan, np.nan, np.nan], "col2": [1, 2, 3]})] ) -def test_Imputer_fit_transform_on_nan_column(df: pd.DataFrame, imputer: imputers._Imputer) -> None: +def test_Imputer_fit_transform_on_nan_column( + df: pd.DataFrame, imputer: imputers._Imputer +) -> None: np.testing.assert_raises(ValueError, imputer.fit_transform, df) @@ -130,7 +143,9 @@ def test_fit_transform_on_grouped(df: pd.DataFrame) -> None: @pytest.mark.parametrize("df", [df_incomplete]) @pytest.mark.parametrize("df_oracle", [df_complete]) -def test_ImputerOracle_fit_transform(df: pd.DataFrame, df_oracle: pd.DataFrame) -> None: +def test_ImputerOracle_fit_transform( + df: pd.DataFrame, df_oracle: pd.DataFrame +) -> None: imputer = imputers.ImputerOracle() imputer.set_solution(df_oracle) result = imputer.fit_transform(df) @@ -142,7 +157,9 @@ def test_ImputerOracle_fit_transform(df: pd.DataFrame, df_oracle: pd.DataFrame) def test_ImputerSimple_mean_fit_transform(df: pd.DataFrame) -> None: imputer = imputers.ImputerSimple(strategy="mean") result = imputer.fit_transform(df) - expected = pd.DataFrame({"col1": [0, 5 / 3, 2, 3, 5 / 3], "col2": ["a", "b", "b", "b", "b"]}) + expected = pd.DataFrame( + {"col1": [0, 5 / 3, 2, 3, 5 / 3], "col2": ["a", "b", "b", "b", "b"]} + ) pd.testing.assert_frame_equal(result, expected) @@ -150,7 +167,9 @@ def test_ImputerSimple_mean_fit_transform(df: pd.DataFrame) -> None: def test_ImputerSimple_median_fit_transform(df: pd.DataFrame) -> None: imputer = imputers.ImputerSimple() result = imputer.fit_transform(df) - expected = pd.DataFrame({"col1": [0.0, 2.0, 2.0, 3.0, 2.0], "col2": ["a", "b", "b", "b", "b"]}) + expected = pd.DataFrame( + {"col1": [0.0, 2.0, 2.0, 3.0, 2.0], "col2": ["a", "b", "b", "b", "b"]} + ) pd.testing.assert_frame_equal(result, expected) @@ -158,7 +177,9 @@ def test_ImputerSimple_median_fit_transform(df: pd.DataFrame) -> None: def test_ImputerSimple_mode_fit_transform(df: pd.DataFrame) -> None: imputer = imputers.ImputerSimple(strategy="most_frequent") result = imputer.fit_transform(df) - expected = pd.DataFrame({"col1": [0.0, 0.0, 2.0, 3.0, 0.0], "col2": ["a", "b", "b", "b", "b"]}) + expected = pd.DataFrame( + {"col1": [0.0, 0.0, 2.0, 3.0, 0.0], "col2": ["a", "b", "b", "b", "b"]} + ) pd.testing.assert_frame_equal(result, expected) @@ -174,7 +195,9 @@ def test_ImputerShuffle_fit_transform1(df: pd.DataFrame) -> None: def test_ImputerShuffle_fit_transform2(df: pd.DataFrame) -> None: imputer = imputers.ImputerShuffle(random_state=42) result = imputer.fit_transform(df) - expected = pd.DataFrame({"col1": [0, 3, 2, 3, 0], "col2": [-1, 1.5, 0.5, 1.5, 1.5]}) + expected = pd.DataFrame( + {"col1": [0, 3, 2, 3, 0], "col2": [-1, 1.5, 0.5, 1.5, 1.5]} + ) np.testing.assert_allclose(result, expected) @@ -182,7 +205,9 @@ def test_ImputerShuffle_fit_transform2(df: pd.DataFrame) -> None: def test_ImputerLOCF_fit_transform(df: pd.DataFrame) -> None: imputer = imputers.ImputerLOCF() result = imputer.fit_transform(df) - expected = pd.DataFrame({"col1": [0, 0, 2, 3, 3], "col2": [-1, -1, 0.5, 0.5, 1.5]}) + expected = pd.DataFrame( + {"col1": [0, 0, 2, 3, 3], "col2": [-1, -1, 0.5, 0.5, 1.5]} + ) np.testing.assert_allclose(result, expected) @@ -190,7 +215,9 @@ def test_ImputerLOCF_fit_transform(df: pd.DataFrame) -> None: def test_ImputerNOCB_fit_transform(df: pd.DataFrame) -> None: imputer = imputers.ImputerNOCB() result = imputer.fit_transform(df) - expected = pd.DataFrame({"col1": [0, 2, 2, 3, 3], "col2": [-1, 0.5, 0.5, 1.5, 1.5]}) + expected = pd.DataFrame( + {"col1": [0, 2, 2, 3, 3], "col2": [-1, 0.5, 0.5, 1.5, 1.5]} + ) np.testing.assert_allclose(result, expected) @@ -198,7 +225,9 @@ def test_ImputerNOCB_fit_transform(df: pd.DataFrame) -> None: def test_ImputerInterpolation_fit_transform(df: pd.DataFrame) -> None: imputer = imputers.ImputerInterpolation() result = imputer.fit_transform(df) - expected = pd.DataFrame({"col1": [0, 1, 2, 3, 3], "col2": [-1, -0.25, 0.5, 1, 1.5]}) + expected = pd.DataFrame( + {"col1": [0, 1, 2, 3, 3], "col2": [-1, -0.25, 0.5, 1, 1.5]} + ) np.testing.assert_allclose(result, expected) @@ -208,8 +237,8 @@ def test_ImputerResiduals_fit_transform(df: pd.DataFrame) -> None: result = imputer.fit_transform(df) expected = pd.DataFrame( { - "col1": [i for i in range(20)], - "col2": [0, 0.953, 2, 2.061, 2] + [i for i in range(5, 20)], + "col1": list(range(20)), + "col2": [0, 0.953, 2, 2.061, 2] + list(range(5, 20)), }, index=pd.date_range("2023-04-17", periods=20, freq="D"), ) @@ -262,14 +291,18 @@ def test_ImputerRegressor_fit_transform(df: pd.DataFrame) -> None: @pytest.mark.parametrize("df", [df_timeseries]) def test_ImputerRpcaNoisy_fit_transform(df: pd.DataFrame) -> None: - imputer = imputers.ImputerRpcaNoisy(columnwise=False, max_iterations=100, tau=1, lam=0.3) + imputer = imputers.ImputerRpcaNoisy( + columnwise=False, max_iterations=100, tau=1, lam=0.3 + ) df_omega = df.notna() df_result = imputer.fit_transform(df) np.testing.assert_allclose(df_result[df_omega], df[df_omega]) assert df_result.notna().all().all() -index_grouped = pd.MultiIndex.from_product([["a", "b"], range(4)], names=["group", "date"]) +index_grouped = pd.MultiIndex.from_product( + [["a", "b"], range(4)], names=["group", "date"] +) dict_values = { "col1": [0, np.nan, 0, np.nan, 1, 1, 1, 1], "col2": [1, 1, 1, 1, 2, 2, 2, 2], @@ -319,6 +352,8 @@ def test_models_fit_transform_grouped(imputer): imputers.ImputerEM(), ] ) -def test_sklearn_compatible_estimator(estimator: imputers._Imputer, check: Any) -> None: +def test_sklearn_compatible_estimator( + estimator: imputers._Imputer, check: Any +) -> None: """Check compatibility with sklearn, using sklearn estimator checks API.""" check(estimator) diff --git a/tests/imputations/test_imputers_diffusions.py b/tests/imputations/test_imputers_diffusions.py index 18363175..40215091 100644 --- a/tests/imputations/test_imputers_diffusions.py +++ b/tests/imputations/test_imputers_diffusions.py @@ -1,10 +1,11 @@ +from typing import Any + import numpy as np import pandas as pd import pytest - -from typing import Any - -from sklearn.utils.estimator_checks import check_estimator, parametrize_with_checks +from sklearn.utils.estimator_checks import ( + parametrize_with_checks, +) from qolmat.benchmark import metrics from qolmat.imputations import imputers, imputers_pytorch @@ -82,7 +83,9 @@ def test_TabDDPM_fit(df: pd.DataFrame) -> None: ) model = ddpms.TabDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64) - model = model.fit(df, batch_size=2, epochs=2, x_valid=df, print_valid=False) + model = model.fit( + df, batch_size=2, epochs=2, x_valid=df, print_valid=False + ) df_imputed = model.predict(df) @@ -94,7 +97,6 @@ def test_TabDDPM_fit(df: pd.DataFrame) -> None: @pytest.mark.parametrize("df", [df_incomplete]) def test_TabDDPM_process_data(df: pd.DataFrame) -> None: - model = ddpms.TabDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64) arr_processed, arr_mask, _ = model._process_data(df, is_training=True) @@ -104,11 +106,14 @@ def test_TabDDPM_process_data(df: pd.DataFrame) -> None: @pytest.mark.parametrize("df", [df_incomplete]) def test_TabDDPM_process_reversely_data(df: pd.DataFrame) -> None: - model = ddpms.TabDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64) - model = model.fit(df, batch_size=2, epochs=2, x_valid=df, print_valid=False) + model = model.fit( + df, batch_size=2, epochs=2, x_valid=df, print_valid=False + ) - arr_processed, arr_mask, list_indices = model._process_data(df, is_training=False) + arr_processed, arr_mask, list_indices = model._process_data( + df, is_training=False + ) df_imputed = model._process_reversely_data(arr_processed, df, list_indices) np.testing.assert_array_equal(df.shape, df_imputed.shape) @@ -118,11 +123,16 @@ def test_TabDDPM_process_reversely_data(df: pd.DataFrame) -> None: @pytest.mark.parametrize("df", [df_incomplete]) def test_TabDDPM_q_sample(df: pd.DataFrame) -> None: - model = ddpms.TabDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64) - model = model.fit(df, batch_size=2, epochs=2, x_valid=df, print_valid=False) + model = model.fit( + df, batch_size=2, epochs=2, x_valid=df, print_valid=False + ) - device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") + device = ( + torch.device("cuda") + if torch.cuda.is_available() + else torch.device("cpu") + ) ts_data_noised, ts_noise = model._q_sample( x=torch.ones(2, 5, dtype=torch.float).to(device), @@ -135,7 +145,9 @@ def test_TabDDPM_q_sample(df: pd.DataFrame) -> None: @pytest.mark.parametrize("df", [df_incomplete]) def test_TabDDPM_eval(df: pd.DataFrame) -> None: - model = ddpms.TabDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64, is_clip=True) + model = ddpms.TabDDPM( + num_noise_steps=10, num_blocks=1, dim_embedding=64, is_clip=True + ) model = model.fit( df, batch_size=2, @@ -156,7 +168,9 @@ def test_TabDDPM_eval(df: pd.DataFrame) -> None: list(df.index), ) - np.testing.assert_array_equal(list(scores.keys()), ["mean_absolute_error", "dist_wasserstein"]) + np.testing.assert_array_equal( + list(scores.keys()), ["mean_absolute_error", "dist_wasserstein"] + ) @pytest.mark.parametrize("df", [df_incomplete]) @@ -191,8 +205,12 @@ def test_TabDDPM_predict(df: pd.DataFrame) -> None: } ) - model = ddpms.TabDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64, is_clip=True) - model = model.fit(df, batch_size=2, epochs=2, x_valid=df, print_valid=False) + model = ddpms.TabDDPM( + num_noise_steps=10, num_blocks=1, dim_embedding=64, is_clip=True + ) + model = model.fit( + df, batch_size=2, epochs=2, x_valid=df, print_valid=False + ) df_imputed = model.predict(df) @@ -216,7 +234,12 @@ def test_TsDDPM_fit(df: pd.DataFrame) -> None: model = ddpms.TsDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64) model = model.fit( - df, batch_size=2, epochs=2, x_valid=df, print_valid=False, index_datetime="datetime" + df, + batch_size=2, + epochs=2, + x_valid=df, + print_valid=False, + index_datetime="datetime", ) df_imputed = model.predict(df) @@ -229,10 +252,16 @@ def test_TsDDPM_fit(df: pd.DataFrame) -> None: @pytest.mark.parametrize("df", [df_incomplete]) def test_TsDDPM_process_data(df: pd.DataFrame) -> None: - - model = ddpms.TsDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64, is_rolling=False) + model = ddpms.TsDDPM( + num_noise_steps=10, num_blocks=1, dim_embedding=64, is_rolling=False + ) model = model.fit( - df, batch_size=2, epochs=2, x_valid=df, print_valid=False, index_datetime="datetime" + df, + batch_size=2, + epochs=2, + x_valid=df, + print_valid=False, + index_datetime="datetime", ) arr_processed, arr_mask, _ = model._process_data(df, is_training=True) @@ -240,9 +269,16 @@ def test_TsDDPM_process_data(df: pd.DataFrame) -> None: np.testing.assert_array_equal(arr_processed.shape, [5, 1, 5]) np.testing.assert_array_equal(arr_mask.shape, [5, 1, 5]) - model = ddpms.TsDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64, is_rolling=True) + model = ddpms.TsDDPM( + num_noise_steps=10, num_blocks=1, dim_embedding=64, is_rolling=True + ) model = model.fit( - df, batch_size=2, epochs=2, x_valid=df, print_valid=False, index_datetime="datetime" + df, + batch_size=2, + epochs=2, + x_valid=df, + print_valid=False, + index_datetime="datetime", ) arr_processed, arr_mask, _ = model._process_data(df, is_training=True) @@ -253,25 +289,42 @@ def test_TsDDPM_process_data(df: pd.DataFrame) -> None: @pytest.mark.parametrize("df", [df_incomplete]) def test_TsDDPM_process_reversely_data(df: pd.DataFrame) -> None: - - model = ddpms.TsDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64, is_rolling=False) + model = ddpms.TsDDPM( + num_noise_steps=10, num_blocks=1, dim_embedding=64, is_rolling=False + ) model = model.fit( - df, batch_size=2, epochs=2, x_valid=df, print_valid=False, index_datetime="datetime" + df, + batch_size=2, + epochs=2, + x_valid=df, + print_valid=False, + index_datetime="datetime", ) - arr_processed, arr_mask, list_indices = model._process_data(df, is_training=False) + arr_processed, arr_mask, list_indices = model._process_data( + df, is_training=False + ) df_imputed = model._process_reversely_data(arr_processed, df, list_indices) np.testing.assert_array_equal(df.shape, df_imputed.shape) np.testing.assert_array_equal(df.index, df_imputed.index) np.testing.assert_array_equal(df.columns, df_imputed.columns) - model = ddpms.TsDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64, is_rolling=True) + model = ddpms.TsDDPM( + num_noise_steps=10, num_blocks=1, dim_embedding=64, is_rolling=True + ) model = model.fit( - df, batch_size=2, epochs=2, x_valid=df, print_valid=False, index_datetime="datetime" + df, + batch_size=2, + epochs=2, + x_valid=df, + print_valid=False, + index_datetime="datetime", ) - arr_processed, arr_mask, list_indices = model._process_data(df, is_training=False) + arr_processed, arr_mask, list_indices = model._process_data( + df, is_training=False + ) df_imputed = model._process_reversely_data(arr_processed, df, list_indices) np.testing.assert_array_equal(df.shape, df_imputed.shape) @@ -281,12 +334,20 @@ def test_TsDDPM_process_reversely_data(df: pd.DataFrame) -> None: @pytest.mark.parametrize("df", [df_incomplete]) def test_TsDDPM_q_sample(df: pd.DataFrame) -> None: - model = ddpms.TsDDPM(num_noise_steps=10, num_blocks=1, dim_embedding=64) model = model.fit( - df, batch_size=2, epochs=2, x_valid=df, print_valid=False, index_datetime="datetime" + df, + batch_size=2, + epochs=2, + x_valid=df, + print_valid=False, + index_datetime="datetime", + ) + device = ( + torch.device("cuda") + if torch.cuda.is_available() + else torch.device("cpu") ) - device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") ts_data_noised, ts_noise = model._q_sample( x=torch.ones(2, 1, 5, dtype=torch.float).to(device), @@ -299,9 +360,13 @@ def test_TsDDPM_q_sample(df: pd.DataFrame) -> None: @parametrize_with_checks( [ - imputers_pytorch.ImputerDiffusion(model=ddpms.TabDDPM(), batch_size=1, epochs=1), + imputers_pytorch.ImputerDiffusion( + model=ddpms.TabDDPM(), batch_size=1, epochs=1 + ), ] ) -def test_sklearn_compatible_estimator(estimator: imputers._Imputer, check: Any) -> None: +def test_sklearn_compatible_estimator( + estimator: imputers._Imputer, check: Any +) -> None: """Check compatibility with sklearn, using sklearn estimator checks API.""" check(estimator) diff --git a/tests/imputations/test_imputers_pytorch.py b/tests/imputations/test_imputers_pytorch.py index a6146291..0704114c 100644 --- a/tests/imputations/test_imputers_pytorch.py +++ b/tests/imputations/test_imputers_pytorch.py @@ -1,7 +1,6 @@ import numpy as np import pandas as pd import pytest -import torch from qolmat.imputations import imputers_pytorch from qolmat.utils.exceptions import PyTorchExtraNotInstalled @@ -29,7 +28,9 @@ def test_ImputerRegressorPyTorch_fit_transform(df: pd.DataFrame) -> None: nn.manual_seed(42) if nn.cuda.is_available(): nn.cuda.manual_seed(42) - estimator = imputers_pytorch.build_mlp(input_dim=2, list_num_neurons=[64, 32]) + estimator = imputers_pytorch.build_mlp( + input_dim=2, list_num_neurons=[64, 32] + ) imputer = imputers_pytorch.ImputerRegressorPyTorch( estimator=estimator, handler_nan="column", epochs=10 ) @@ -55,30 +56,3 @@ def test_ImputerRegressorPyTorch_fit_transform(df: pd.DataFrame) -> None: } ) np.testing.assert_allclose(result, expected, atol=1e-3) - - -# @pytest.mark.parametrize("df", [df_incomplete]) -# def test_imputers_pytorch_Autoencoder(df: pd.DataFrame) -> None: -# input = df.values.shape[1] -# latent = 4 -# encoder, decoder = imputers_pytorch.build_autoencoder_example( -# input_dim=input, -# latent_dim=latent, -# output_dim=input, -# list_num_neurons=[4 * latent, 2 * latent], -# ) -# autoencoder = imputers_pytorch.ImputerAutoencoder( -# encoder, decoder, epochs=10, lamb=0.01, max_iterations=5, random_state=42 -# ) -# result = autoencoder.fit_transform(df) -# print(result) -# expected = pd.DataFrame( -# { -# "col1": [22.315, 15, 22.496, 23, 33], -# "col2": [69, 76, 74, 80, 78], -# "col3": [174, 166, 182, 177, 174.218], -# "col4": [9, 12, 11, 12, 8], -# "col5": [93, 75, 62.308, 12, 62.449], -# } -# ) -# np.testing.assert_allclose(result, expected, atol=1e-3) diff --git a/tests/imputations/test_preprocessing.py b/tests/imputations/test_preprocessing.py index 30b55bd3..a05fffdb 100644 --- a/tests/imputations/test_preprocessing.py +++ b/tests/imputations/test_preprocessing.py @@ -1,15 +1,12 @@ import numpy as np import pandas as pd import pytest -from sklearn.compose import make_column_selector as selector - -from sklearn.pipeline import Pipeline from sklearn.base import BaseEstimator, TransformerMixin from sklearn.metrics import mean_squared_error -from sklearn.utils.estimator_checks import check_estimator -from sklearn.utils.validation import check_X_y, check_array from sklearn.model_selection import train_test_split -from sklearn.compose import ColumnTransformer +from sklearn.pipeline import Pipeline +from sklearn.utils.estimator_checks import check_estimator + from qolmat.imputations.preprocessing import ( BinTransformer, MixteHGBM, @@ -83,7 +80,9 @@ def test_fit_transform_BinTransformer(bin_transformer): def test_transform_BinTransformer(bin_transformer): bin_transformer.dict_df_bins_ = { - 0: pd.DataFrame({"value": [1, 2, 3, 4, 5], "min": [-np.inf, 1.5, 2.5, 3.5, 4.5]}) + 0: pd.DataFrame( + {"value": [1, 2, 3, 4, 5], "min": [-np.inf, 1.5, 2.5, 3.5, 4.5]} + ) } bin_transformer.feature_names_in_ = pd.Index([0]) bin_transformer.n_features_in_ = 1 @@ -100,7 +99,9 @@ def test_fit_transform_with_dataframes_BinTransformer(bin_transformer): def test_transform_with_dataframes_BinTransformer(bin_transformer): bin_transformer.dict_df_bins_ = { - 0: pd.DataFrame({"value": [1, 2, 3, 4, 5], "min": [0.5, 1.5, 2.5, 3.5, 4.5]}) + 0: pd.DataFrame( + {"value": [1, 2, 3, 4, 5], "min": [0.5, 1.5, 2.5, 3.5, 4.5]} + ) } bin_transformer.feature_names_in_ = pd.Index(["0"]) bin_transformer.n_features_in_ = 1 @@ -126,7 +127,9 @@ def test_inverse_transform_OneHotEncoderProjector(encoder): df_back = encoder.inverse_transform(df_dum) pd.testing.assert_frame_equal(df, df_back) - df_dum_perturbated = df_dum + np.random.uniform(-0.5, 0.5, size=df_dum.shape) + df_dum_perturbated = df_dum + np.random.uniform( + -0.5, 0.5, size=df_dum.shape + ) df_back = encoder.inverse_transform(df_dum_perturbated) pd.testing.assert_frame_equal(df, df_back) @@ -137,16 +140,22 @@ def test_inverse_transform_OneHotEncoderProjector(encoder): class DummyTransformer(TransformerMixin, BaseEstimator): + """Dummy transformer for testing.""" + def fit(self, X, y=None): + """Fit function.""" return self def transform(self, X): + """Transform function.""" return X def fit_transform(self, X, y=None): + """Fit and transform function.""" return self.fit(X, y).transform(X) def inverse_transform(self, X, y=None): + """Inverse transform function.""" return X diff --git a/tests/imputations/test_softimpute.py b/tests/imputations/test_softimpute.py index e8c3dff0..b85025da 100644 --- a/tests/imputations/test_softimpute.py +++ b/tests/imputations/test_softimpute.py @@ -1,4 +1,3 @@ -from typing import Any import numpy as np import pytest from numpy.typing import NDArray @@ -10,16 +9,16 @@ X_non_regression_test = np.array( [[1, 2, np.nan, 4], [1, 5, 3, np.nan], [4, 2, 3, 2], [1, 1, 5, 4]] ) -X_expected = np.array([[1, 2, 2.9066, 4], [1, 5, 3, 2.1478], [4, 2, 3, 2], [1, 1, 5, 4]]) +X_expected = np.array( + [[1, 2, 2.9066, 4], [1, 5, 3, 2.1478], [4, 2, 3, 2], [1, 1, 5, 4]] +) tau = 1 max_iterations = 30 random_state = 50 def test_initialized_default() -> None: - """Test that initialization does not crash and - has default parameters - """ + """Test that initialization does not crash and has default parameters.""" model = softimpute.SoftImpute() assert model.period == 1 assert model.rank is None @@ -27,9 +26,7 @@ def test_initialized_default() -> None: def test_initialized_custom() -> None: - """Test that initialization does not crash and - has custom parameters - """ + """Test that initialization does not crash and has custom parameters.""" model = softimpute.SoftImpute(period=2, rank=10) assert model.period == 2 assert model.rank == 10 @@ -38,13 +35,17 @@ def test_initialized_custom() -> None: @pytest.mark.parametrize("X", [X]) def test_soft_impute_decompose(X: NDArray) -> None: - """Test fit instance and decomposition is computed""" + """Test fit instance and decomposition is computed.""" tau = 1 model = softimpute.SoftImpute(tau=tau) Omega = ~np.isnan(X) X_imputed = np.where(Omega, X, 0) - cost_all_in_M = model.cost_function(X, X_imputed, np.full_like(X, 0), Omega, tau) - cost_all_in_A = model.cost_function(X, np.full_like(X, 0), X_imputed, Omega, tau) + cost_all_in_M = model.cost_function( + X, X_imputed, np.full_like(X, 0), Omega, tau + ) + cost_all_in_A = model.cost_function( + X, np.full_like(X, 0), X_imputed, Omega, tau + ) M, A = model.decompose(X, Omega) cost_final = model.cost_function(X, M, A, Omega, tau) assert isinstance(model, softimpute.SoftImpute) @@ -56,12 +57,9 @@ def test_soft_impute_decompose(X: NDArray) -> None: assert cost_final < cost_all_in_A -# tests/imputations/test_imputers.py::test_sklearn_compatible_estimator - - @pytest.mark.parametrize("X", [X]) def test_soft_impute_convergence(X: NDArray) -> None: - """Test type of the check convergence""" + """Test type of the check convergence.""" model = softimpute.SoftImpute() M = model.random_state.uniform(size=(10, 20)) U, D, V = np.linalg.svd(M, full_matrices=False) @@ -70,31 +68,14 @@ def test_soft_impute_convergence(X: NDArray) -> None: def test_soft_impute_convergence_with_none() -> None: - """Test check type None and raise error""" + """Test check type None and raise error.""" model = softimpute.SoftImpute() with pytest.raises(ValueError): _ = model._check_convergence( - None, + np.array([1]), np.array([1]), np.array([1]), np.array([1]), np.array([1]), np.array([1]), ) - - -# @pytest.mark.parametrize( -# "X, X_expected, tau, max_iterations, random_state", -# [(X_non_regression_test, X_expected, tau, max_iterations, random_state)], -# ) -# def test_soft_impute_non_regression( -# X: NDArray, X_expected: NDArray, tau: float, max_iterations: int, random_state: int -# ) -> None: -# """Non regression test""" -# model = softimpute.SoftImpute( -# tau=tau, max_iterations=max_iterations, random_state=random_state -# ) -# Omega = ~np.isnan(X) -# M, A = model.decompose(X, Omega) -# X_result = M + A -# np.testing.assert_allclose(X_result, X_expected, rtol=1e-3, atol=1e-3) diff --git a/tests/utils/test_algebra.py b/tests/utils/test_algebra.py index 45a508c8..d0432219 100644 --- a/tests/utils/test_algebra.py +++ b/tests/utils/test_algebra.py @@ -1,5 +1,4 @@ import numpy as np -from sympy import diag from qolmat.utils import algebra @@ -12,7 +11,9 @@ def test_frechet_distance_exact(): means2 = np.array([0, -1, 1]) cov2 = np.eye(3, 3) - expected = np.sum((means2 - means1) ** 2) + np.sum((np.sqrt(stds) - 1) ** 2) + expected = np.sum((means2 - means1) ** 2) + np.sum( + (np.sqrt(stds) - 1) ** 2 + ) expected /= 3 result = algebra.frechet_distance_exact(means1, cov1, means2, cov2) np.testing.assert_almost_equal(result, expected, decimal=3) @@ -26,6 +27,8 @@ def test_kl_divergence_gaussian_exact(): means2 = np.array([0, -1, 1]) cov2 = np.eye(3, 3) - expected = (np.sum(stds**2 - np.log(stds**2) - 1 + (means2 - means1) ** 2)) / 2 + expected = ( + np.sum(stds**2 - np.log(stds**2) - 1 + (means2 - means1) ** 2) + ) / 2 result = algebra.kl_divergence_gaussian_exact(means1, cov1, means2, cov2) np.testing.assert_almost_equal(result, expected, decimal=3) diff --git a/tests/utils/test_data.py b/tests/utils/test_data.py index 40ee120a..713ff611 100644 --- a/tests/utils/test_data.py +++ b/tests/utils/test_data.py @@ -1,19 +1,40 @@ import datetime import os +from unittest.mock import MagicMock, patch import numpy as np import pandas as pd import pytest from pytest_mock.plugin import MockerFixture -from unittest.mock import MagicMock, patch + from qolmat.utils import data columns = ["station", "date", "year", "month", "day", "hour", "a", "b", "wd"] df_beijing_raw = pd.DataFrame( [ ["Beijing", datetime.datetime(2013, 3, 1), 2013, 3, 1, 0, 1, 2, "NW"], - ["Beijing", datetime.datetime(2013, 3, 1), 2014, 3, 1, 0, 3, np.nan, "NW"], - ["Beijing", datetime.datetime(2013, 3, 1), 2015, 3, 1, 0, np.nan, 6, "NW"], + [ + "Beijing", + datetime.datetime(2013, 3, 1), + 2014, + 3, + 1, + 0, + 3, + np.nan, + "NW", + ], + [ + "Beijing", + datetime.datetime(2013, 3, 1), + 2015, + 3, + 1, + 0, + np.nan, + 6, + "NW", + ], ], columns=columns, ) @@ -71,7 +92,13 @@ [2.0, 5.0, 4.0, 1.0, 4.0], [3.0, 6.0, 3.0, 4.0, 6.0], ], - columns=["T1 rain", "T2 preasure", "T3 temperature", "T4 humidity", "T5 sun"], + columns=[ + "T1 rain", + "T2 preasure", + "T3 temperature", + "T4 humidity", + "T5 sun", + ], index=pd.date_range(start="2010-01-01", periods=3, freq="1D"), ) @@ -222,7 +249,9 @@ def test_get_dataframes_in_folder(mock_convert_tsf, mock_read_csv, mock_walk): mock_walk.return_value = [("/fakepath", ("subfolder",), ("file.csv",))] result_csv = data.get_dataframes_in_folder("/fakepath", ".csv") assert len(result_csv) == 1 - mock_read_csv.assert_called_once_with(os.path.join("/fakepath", "file.csv")) + mock_read_csv.assert_called_once_with( + os.path.join("/fakepath", "file.csv") + ) pd.testing.assert_frame_equal(result_csv[0], df_conductor) mock_read_csv.reset_mock() @@ -230,7 +259,9 @@ def test_get_dataframes_in_folder(mock_convert_tsf, mock_read_csv, mock_walk): mock_walk.return_value = [("/fakepath", ("subfolder",), ("file.tsf",))] result_tsf = data.get_dataframes_in_folder("/fakepath", ".tsf") assert len(result_tsf) == 1 - mock_convert_tsf.assert_called_once_with(os.path.join("/fakepath", "file.tsf")) + mock_convert_tsf.assert_called_once_with( + os.path.join("/fakepath", "file.tsf") + ) pd.testing.assert_frame_equal(result_tsf[0], df_beijing) mock_read_csv.assert_called() @@ -238,14 +269,18 @@ def test_get_dataframes_in_folder(mock_convert_tsf, mock_read_csv, mock_walk): @patch("numpy.random.normal") @patch("numpy.random.choice") @patch("numpy.random.standard_exponential") -def test_generate_artificial_ts(mock_standard_exponential, mock_choice, mock_normal): +def test_generate_artificial_ts( + mock_standard_exponential, mock_choice, mock_normal +): n_samples = 100 periods = [10, 20] amp_anomalies = 1.0 ratio_anomalies = 0.1 amp_noise = 0.1 - mock_standard_exponential.return_value = np.ones(int(n_samples * ratio_anomalies)) + mock_standard_exponential.return_value = np.ones( + int(n_samples * ratio_anomalies) + ) mock_choice.return_value = np.arange(int(n_samples * ratio_anomalies)) mock_normal.return_value = np.zeros(n_samples) @@ -274,11 +309,20 @@ def test_generate_artificial_ts(mock_standard_exponential, mock_choice, mock_nor ("Bug", None), ], ) -def test_data_get_data(name_data: str, df: pd.DataFrame, mocker: MockerFixture) -> None: - mock_download = mocker.patch("qolmat.utils.data.download_data_from_zip", return_value=[df]) - mock_read = mocker.patch("qolmat.utils.data.read_csv_local", return_value=df) +def test_data_get_data( + name_data: str, df: pd.DataFrame, mocker: MockerFixture +) -> None: + mock_download = mocker.patch( + "qolmat.utils.data.download_data_from_zip", return_value=[df] + ) + mock_read = mocker.patch( + "qolmat.utils.data.read_csv_local", return_value=df + ) mock_read_dl = mocker.patch("pandas.read_csv", return_value=df) - mocker.patch("qolmat.utils.data.preprocess_data_beijing", return_value=df_preprocess_beijing) + mocker.patch( + "qolmat.utils.data.preprocess_data_beijing", + return_value=df_preprocess_beijing, + ) mocker.patch("pandas.read_parquet", return_value=df_sncf) try: @@ -346,7 +390,9 @@ def test_preprocess_data_beijing(df: pd.DataFrame) -> None: assert result_df.index.names == ["station", "datetime"] assert all(result_df.index.get_level_values("station") == "Beijing") assert len(result_df) == 1 - assert np.isclose(result_df.loc[(("Beijing"),), "pm2.5"], 176.66666666666666) + assert np.isclose( + result_df.loc[(("Beijing"),), "pm2.5"], 176.66666666666666 + ) @pytest.mark.parametrize("df", [df_preprocess_offline]) @@ -363,7 +409,9 @@ def test_data_add_holes(df: pd.DataFrame) -> None: ("Beijing", df_beijing), ], ) -def test_data_get_data_corrupted(name_data: str, df: pd.DataFrame, mocker: MockerFixture) -> None: +def test_data_get_data_corrupted( + name_data: str, df: pd.DataFrame, mocker: MockerFixture +) -> None: mock_get = mocker.patch("qolmat.utils.data.get_data", return_value=df) df_out = data.get_data_corrupted(name_data) assert mock_get.call_count == 1 @@ -395,5 +443,7 @@ def test_data_add_datetime_features(df: pd.DataFrame) -> None: result = data.add_datetime_features(df) pd.testing.assert_index_equal(result.index, df.index) assert result.columns.tolist() == columns_out - pd.testing.assert_frame_equal(result.drop(columns=["time_cos", "time_sin"]), df) + pd.testing.assert_frame_equal( + result.drop(columns=["time_cos", "time_sin"]), df + ) assert (result["time_cos"] ** 2 + result["time_sin"] ** 2 == 1).all() diff --git a/tests/utils/test_exceptions.py b/tests/utils/test_exceptions.py index e9e10b7a..e0703c7f 100644 --- a/tests/utils/test_exceptions.py +++ b/tests/utils/test_exceptions.py @@ -1,4 +1,3 @@ -import pytest from qolmat.utils import exceptions diff --git a/tests/utils/test_plot.py b/tests/utils/test_plot.py index 5c45e72e..aadbaf7f 100644 --- a/tests/utils/test_plot.py +++ b/tests/utils/test_plot.py @@ -1,13 +1,14 @@ from typing import Any, List, Tuple -import matplotlib as mpl + import matplotlib.pyplot as plt import numpy as np import pandas as pd import pytest import scipy.sparse -from qolmat.utils import plot from pytest_mock.plugin import MockerFixture +from qolmat.utils import plot + plt.switch_backend("Agg") np.random.seed(42) @@ -30,12 +31,16 @@ df1 = pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]}) df2 = pd.DataFrame({"x": [2, 3, 4], "y": [5, 6, 7]}) dict_df_imputed = { - "Imputer1": pd.DataFrame({"A": [2, 3, np.nan], "B": [5, np.nan, 7], "C": [np.nan, 8, 9]}) + "Imputer1": pd.DataFrame( + {"A": [2, 3, np.nan], "B": [5, np.nan, 7], "C": [np.nan, 8, 9]} + ) } @pytest.mark.parametrize("list_matrices", [list_matrices]) -def test_utils_plot_plot_matrices(list_matrices: List[np.ndarray], mocker: MockerFixture) -> None: +def test_utils_plot_plot_matrices( + list_matrices: List[np.ndarray], mocker: MockerFixture +) -> None: mocker.patch("matplotlib.pyplot.savefig") mocker.patch("matplotlib.pyplot.show") plot.plot_matrices(list_matrices=list_matrices, title="title") @@ -45,7 +50,9 @@ def test_utils_plot_plot_matrices(list_matrices: List[np.ndarray], mocker: Mocke @pytest.mark.parametrize("list_signals", [list_signals]) -def test_utils_plot_plot_signal(list_signals: List[List[Any]], mocker: MockerFixture) -> None: +def test_utils_plot_plot_signal( + list_signals: List[List[Any]], mocker: MockerFixture +) -> None: mocker.patch("matplotlib.pyplot.savefig") mocker.patch("matplotlib.pyplot.show") plot.plot_signal(list_signals=list_signals, ylabel="ylabel", title="title") @@ -54,7 +61,9 @@ def test_utils_plot_plot_signal(list_signals: List[List[Any]], mocker: MockerFix plt.close("all") -@pytest.mark.parametrize("M, A, E, index_array, dims", [(M, A, E, [0, 1, 2], (10, 10))]) +@pytest.mark.parametrize( + "M, A, E, index_array, dims", [(M, A, E, [0, 1, 2], (10, 10))] +) def test__utils_plot_plot_images( M: np.ndarray, A: np.ndarray, @@ -72,7 +81,9 @@ def test__utils_plot_plot_images( @pytest.mark.parametrize("X", [X]) -def test_utils_plot_make_ellipses_from_data(X: np.ndarray, mocker: MockerFixture): +def test_utils_plot_make_ellipses_from_data( + X: np.ndarray, mocker: MockerFixture +): mocker.patch("matplotlib.pyplot.show") ax = plt.gca() plot.make_ellipses_from_data(X[1], X[2], ax, color="blue") @@ -93,7 +104,9 @@ def test_utils_plot_compare_covariances( @pytest.mark.parametrize("df", [df]) @pytest.mark.parametrize("orientation", ["horizontal", "vertical"]) -def test_utils_plot_multibar(df: pd.DataFrame, orientation: str, mocker: MockerFixture): +def test_utils_plot_multibar( + df: pd.DataFrame, orientation: str, mocker: MockerFixture +): mocker.patch("matplotlib.pyplot.show") plot.multibar(df, orientation=orientation) assert len(plt.gcf().get_axes()) > 0 diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 950d2bf0..4f048d10 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -1,20 +1,21 @@ import sys +from io import StringIO + import numpy as np -from numpy.typing import NDArray import pandas as pd import pytest -from qolmat.utils import utils -from pytest_mock.plugin import MockerFixture -from io import StringIO - -from qolmat.utils.exceptions import NotDimension2, SignalTooShort +from numpy.typing import NDArray +from qolmat.utils import utils +from qolmat.utils.exceptions import NotDimension2 df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]}) @pytest.mark.parametrize("iteration, total", [(1, 1)]) -def test_utils_utils_display_progress_bar(iteration: int, total: int, capsys) -> None: +def test_utils_utils_display_progress_bar( + iteration: int, total: int, capsys +) -> None: captured_output = StringIO() sys.stdout = captured_output utils.progress_bar( @@ -34,7 +35,9 @@ def test_utils_utils_display_progress_bar(iteration: int, total: int, capsys) -> assert output == output_expected -@pytest.mark.parametrize("values, lag_max", [(pd.Series([1.0, 2.0, 3.0, 4.0, 5.0]), 3)]) +@pytest.mark.parametrize( + "values, lag_max", [(pd.Series([1.0, 2.0, 3.0, 4.0, 5.0]), 3)] +) def test_utils_utils_acf(values, lag_max): result = utils.acf(values, lag_max) result_expected = pd.Series([1.0, 1.0, 1.0])