diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7c866fdb..34371413 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,17 +33,16 @@ repos: types: [python] - repo: local hooks: - - id: ruff - name: 'ruff: Check for errors, styling issues and complexity' + - id: ruff-check + name: 'Ruff: Check for errors, styling issues and complexity, and fixes issues if possible (including import order)' entry: ruff language: system - - repo: local - hooks: - - id: isort - name: 'isort: Sort file imports' - entry: isort + args: [ --fix, --no-cache ] + - id: ruff-format + name: 'Ruff: format code in line with PEP8' + entry: ruff format language: system - types: [python] + args: [ --no-cache ] - repo: local hooks: - id: codespell @@ -57,12 +56,4 @@ repos: hooks: - id: pyupgrade name: 'pyupgrade: Updates code to Python 3.8+ code convention' - args: [*py_version] - - repo: local - hooks: - - id: black - name: 'black: PEP8 compliant code formatter' - entry: black - language: python - types: [python] - language_version: python3 \ No newline at end of file + args: [*py_version] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 5a07bfa9..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,216 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [2.1.1] - 2023-09 -Improvements in this release: -- Update SHAP version to the latest #228 - -## [2.1.0] - 2023-07 -Improvements in this release: -- Make ShapRFECV return matplotfigure (instead of axis) #222 -- Add option for penalty on shap calculation to distinguish features with similar shap performance # 213 -- Implement automatic feature selection #220 - -## [2.0.1] - 2023-06 -Improvements in this release: -- Update pre-commit hooks & add validation for jupyter notebooks # 213 -- Fix the docs deployment #211 - -## [2.0.0] - 2023-06 -Improvements in this release: -- Drop explicit support for python 3.7, add support for 3.11 #206, #203, #185 -- Activate and add pre-commit hooks (isort, codespell) #205, #206 -- Add support for groups in SHAP RFECV #182 -- Bug fix: SHAP RFECV now produces reproducible results every time (this breaks backwards compatibility) #197 -- Bug fix: Updated GitHub actions, fixed deprecations #199 -- Bug fix: Remove most of the unreliable warning assertion checks #207 - -## [1.8.9] - 2022-04-08 -Improvements in this release: -- Drop explicit support for python 3.6, add 3.10 #177 -- Bug fix: define shap mask based on rows, instead of columns #178 -- Bug fixes in unit tests #180 -- Improve support for categorical features in shap calculations #184 - -## [1.8.8] - 2021-12-08 -Improvements in this release: -- Added support for XGBoost and Catboost models in ShapRFECV #175 - -## [1.8.7] - 2021-10-28 -Improvements in this release: -- Added support for early stopping in new lightgbm version #164 - -## [1.8.6] - 2021-10-05 -Improvements in this release: -- Added alpha parameter to DependencePlotter #162 - -## [1.8.5] - 2021-08-24 -Improvements in this release: -- Docs and docstrings improvements for stats tests #158 - -## [1.8.4] - 2021-06-16 -Improvements in this release: -- Fix the bug in the Shap Dependence Plot #153 -- Add HowTo guide for using grouped data #154 - -## [1.8.3] - 2021-06-15 -Improvements in this release: -- Fix p-value calculation in PSI #142 - -## [1.8.2] - 2021-05-04 -Improvements in this release: -- Fix catboost bug when calculating SHAP values #147 -- Supply eval_sample_weight for fit in EarlyStoppingShapRFECV #144 -- Remove codecov.io #145 -- Remove sample_row from probatus #140 - -## [1.8.1] - 2021-04-18 -Improvements in this release: -- Enable use of sample_weight in ShapRFECV and EarlyStoppingShapRFECV #139 -- Fix bug in EarlyStoppingShapRFECV #139 -- Fix issue with categorical features in SHAP #138 -- Missing values handled by AutoDist #126 -- Fix issue with missing histogram in DependencePlot #137 - -## [1.8.0] - 2021-04-14 -Improvements in this release: -- Implemented EarlyStoppingShapRFECV #108 -- Added support for Python 3.9 #132 - -## [1.7.1] - 2021-04-13 -Improvements in this release: -- Add error if model pipeline passed to SHAP #129 -- Fixed PSI bug with empty bins #116 -- Unit tests are run daily #113 -- TreeBucketer has been refactored #124 -- Fixes to failing test pipeline #120 -- Improving language in docs #109, #107 - -## [1.7.0] - 2021-03-16 -Improvements in this release: -- Create a comparison of imputation strategies #86 -- Added support for passing check_additivity argument #103 -- Range of code styling issues fixed, based on precommit config #100 -- Renamed TreeDependencePlotter to DependencePlotter and exposed the docs #94 -- Enable instalation of extra dependencies #97 -- Added how to notebook to ensure reproducibility #99 -- Description of vision of probatus #91 - -## [1.6.2] - 2021-03-10 -Improvements in this release: -- Bugfix, allow passing kwargs to dependence plot in ShapModelInterpreter #90 - -## [1.6.1] - 2021-03-09 -Improvements in this release: -- Added ShapRFECV support for all sklearn compatible search CVs. #76 #49 - -## [1.6.0] - 2021-03-01 -Improvements in this release: -- Added features list to README #53 -- Added docs for sample row functionality #54 -- Added 'open in colab' badges to tutorial notebooks #56 -- Deploy documentation on release #47 -- Added columns_to_keep for shap feature elimination #63 -- Updated docs for usage of columns to keep functionality in SHAPRFECV #66 -- Added shap support for linear models #69 -- Installed probatus in colab notebooks #80 -- Minor infrastructure tweaks #81 - -## [1.5.1] - 2020-12-04 - -Various improvements to the consistency and usability of the package -- Unit test docstring and notebooks #41 -- Unified scoring metric within probatus #27 -- Improve docstrings consistency documentation #25 -- Implemented unified interface #24 -- Added images to API docs documentation #23 -- Added verbose parameter to ShapRFECV #21 -- Make API more consistent #19 - - Set model parameter name to clf across probatus - - Set default random_state to None - - Ensure that verbose is used consistently in probatus - - Unify parameter class_names for classes in which it is relevant - - Add return scores parameter to compute wherever applicable -- Add sample row functionality to utils #17 -- Make an experiment comparing sklearn.RFECV with ShapRFECV #16 -- ShapModelInterpreter calculate train set feature importance #13 - -## [1.5.0] - 2020-11-18 -- Improve SHAP RFECV API and documentation - -## [1.4.4] - 2020-11-11 -- Fix issue with the distribution uploaded to pypi - -## [1.4.0] - 2020-11-10 (Broken) -- Add SHAP RFECV for features elimination - -## [1.3.0] - 2020-11-05 (Broken) -- Add SHAP Model Inspector with docs and tests - -## [1.2.0] - 2020-09-30 -- Add resemblance model, with SHAP based importance -- Improve the docs for resemblance model -- Refactor stats tests, improve docs and expose functionality to users - -## [1.1.1] - 2020-09-08 -- Improve Tree Bucketer, enable user to pass own tree object - -## [1.1.0] - 2020-08-24 -- Improve docs for stats_tests -- Refactor stats_tests - -## [1.0.1] - 2020-08-07 -- TreeBucketer, which bins the data based on the target distribution, using Decision Trees fitted on a single feature -- PSI calculation includes the p-values calculation - -## [1.0.0] - 2020-02-24 -- metric_volatility and sample_similarity rebuilt -- New documentation -- Faster tests -- Improved and simplified API -- Scorer class added to the package -- Removed data from repository -- Hiding unfinished functionality from the user - -## [0.1.3] - 2020-02-24 - -### Added - -- VolalityEstimation now has random_seed argument - -### Changed - -- Improved unit testing -- Improved documentation README and CONTRIBUTING - -### Fixed - -- Added dependency on scipy 1.4+ - -## [0.1.2] - 2019-10-29 -### Added - -- Readthedocs documentation website - -## [0.1.1] - 2019-10-09 - -### Added - -- Added CHANGELOG.md - -### Changed - -- Renamed to probatus -- Improved testing by adding pyflakes to CI -- probatus.metric_uncertainty.VolatilityEstimation is now deterministic, added random_state parameter - -## [0.1.0] - 2019-09-21 - -Initial release, commit ecbd0d08a6eea370afda4a4790edeb4ee382995c - -[Unreleased]: https://gitlab.com/ing_rpaa/probatus/compare/ecbd0d08a6eea370afda4a4790edeb4ee382995c...master -[0.1.0]: https://gitlab.com/ing_rpaa/probatus/commit/ecbd0d08a6eea370afda4a4790edeb4ee382995c diff --git a/README.md b/README.md index bd4d37ff..49c70ef9 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,8 @@ **Probatus** is a python package that helps validate binary classification models and the data used to develop them. Main features: - [probatus.interpret](https://ing-bank.github.io/probatus/api/model_interpret.html) provides shap-based model interpretation tools -- [probatus.metric_volatility](https://ing-bank.github.io/probatus/api/metric_volatility.html) provides tools using bootstrapping and/or different random seeds to assess metric volatility/stability. - [probatus.sample_similarity](https://ing-bank.github.io/probatus/api/sample_similarity.html) to compare two datasets using resemblance modelling, f.e. `train` with out-of-time `test`. - [probatus.feature_elimination.ShapRFECV](https://ing-bank.github.io/probatus/api/feature_elimination.html) provides cross-validated Recursive Feature Elimination using shap feature importance. -- [probatus.missing_values](https://ing-bank.github.io/probatus/api/imputation_selector.html) compares performance gains of different missing values imputation strategies for a given model. ## Installation diff --git a/VISION.md b/VISION.md index bb30b6cf..253b6ac9 100644 --- a/VISION.md +++ b/VISION.md @@ -28,5 +28,4 @@ The main principles that drive development of `Probatus` are the following ## The Roadmap -The following [issue](https://github.com/ing-bank/Probatus/issues/93) keeps track of the features coming to Probatus. We are open to new ideas, so if you can think of a feature that fits the vision, make an [issue](https://github.com/ing-bank/Probatus/issues) and help us further develop this package. \ No newline at end of file diff --git a/docs/api/imputation_selector.md b/docs/api/imputation_selector.md deleted file mode 100644 index d4fc675f..00000000 --- a/docs/api/imputation_selector.md +++ /dev/null @@ -1,6 +0,0 @@ -# Imputation Selector - -This module allows us to select imputation strategies. - - -::: probatus.missing_values.imputation diff --git a/docs/api/metric_volatility.md b/docs/api/metric_volatility.md deleted file mode 100644 index 2da631ec..00000000 --- a/docs/api/metric_volatility.md +++ /dev/null @@ -1,12 +0,0 @@ -# Metric Volatility - -The aim of this module is the analysis of how well a model performs on a given dataset, and how stable the performance is. - -The following features are implemented: - -- [TrainTestVolatility][probatus.metric_volatility.volatility.TrainTestVolatility]: Estimation of the volatility of metrics. The estimation is done by splitting the data into train and test multiple times and training and scoring a model based on these metrics. -- [SplitSeedVolatility][probatus.metric_volatility.volatility.SplitSeedVolatility]: Estimates the volatility of metrics based on splitting the data into train and test sets multiple times randomly, each time with a different seed. -- [BootstrappedVolatility][probatus.metric_volatility.volatility.BootstrappedVolatility]: Estimates the volatility of metrics based on splitting the data into train and test with static seed, and bootstrapping the train and test set. - - -::: probatus.metric_volatility.volatility \ No newline at end of file diff --git a/docs/api/stat_tests.md b/docs/api/stat_tests.md deleted file mode 100644 index 55f5ba26..00000000 --- a/docs/api/stat_tests.md +++ /dev/null @@ -1,18 +0,0 @@ -# Statistical Tests - -This module allows us to apply different statistical tests. - -::: probatus.stat_tests.distribution_statistics - -## Available tests -- [Anderson-Darling (ad)][probatus.stat_tests.ad.ad] -- [Epps-Singleton (es)][probatus.stat_tests.es.es] -- [Kolmogorov-Smirnov (ks)][probatus.stat_tests.ks.ks] -- [Population Stability Index (psi)][probatus.stat_tests.psi.psi] -- [Shapiro-Wilk (sw)][probatus.stat_tests.sw.sw] - -::: probatus.stat_tests.ad -::: probatus.stat_tests.es -::: probatus.stat_tests.ks -::: probatus.stat_tests.psi -::: probatus.stat_tests.sw diff --git a/docs/img/KS2_Example.png b/docs/img/KS2_Example.png deleted file mode 100644 index a1d64c14..00000000 Binary files a/docs/img/KS2_Example.png and /dev/null differ diff --git a/docs/img/autodist.png b/docs/img/autodist.png deleted file mode 100644 index b3fc3896..00000000 Binary files a/docs/img/autodist.png and /dev/null differ diff --git a/docs/img/imputation_comparison.png b/docs/img/imputation_comparison.png deleted file mode 100644 index 6050aa6e..00000000 Binary files a/docs/img/imputation_comparison.png and /dev/null differ diff --git a/docs/img/metric_volatility_bootstrapped.png b/docs/img/metric_volatility_bootstrapped.png deleted file mode 100644 index 947f0c4a..00000000 Binary files a/docs/img/metric_volatility_bootstrapped.png and /dev/null differ diff --git a/docs/img/metric_volatility_split_seed.png b/docs/img/metric_volatility_split_seed.png deleted file mode 100644 index 75443b23..00000000 Binary files a/docs/img/metric_volatility_split_seed.png and /dev/null differ diff --git a/docs/img/metric_volatility_train_test.png b/docs/img/metric_volatility_train_test.png deleted file mode 100644 index d780cb9a..00000000 Binary files a/docs/img/metric_volatility_train_test.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md index ffb58f2e..c3ed477b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,3 @@ -# Welcome to probatus documentation! - **Probatus** is a Python library that allows to analyse binary classification models as well as the data used to develop them. diff --git a/docs/tutorials/nb_binning.ipynb b/docs/tutorials/nb_binning.ipynb deleted file mode 100644 index 41f4acb7..00000000 --- a/docs/tutorials/nb_binning.ipynb +++ /dev/null @@ -1,642 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Binning\n", - "\n", - "[![open in colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ing-bank/probatus/blob/master/docs/tutorials/nb_binning.ipynb)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install probatus" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "%config Completer.use_jedi = False\n", - "%load_ext autoreload\n", - "%autoreload 2\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "pd.set_option(\"display.max_columns\", 100)\n", - "pd.set_option(\"display.max_row\", 500)\n", - "pd.set_option(\"display.max_colwidth\", 200)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This notebook explains how the various implemented binning strategies of `probatus` work. \n", - "First, we import all binning strategies:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from probatus.binning import AgglomerativeBucketer, QuantileBucketer, SimpleBucketer, TreeBucketer" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's create some data on which we want to apply the binning strategies. We choose a logistic function because it clearly supports the explanation on how binning strategies work. Moreover, the typical reliability curve for a trained random forest model has this shape and binning strategies could be used for probability calibration (see also the website of Scikit-learn on [probability calibration](https://scikit-learn.org/stable/modules/calibration.html))." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "def log_function(x):\n", - " return 1 / (1 + np.exp(-x))\n", - "\n", - "\n", - "x = [log_function(x) for x in np.arange(-10, 10, 0.01)]\n", - "\n", - "plt.plot(x);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Simple binning" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `SimpleBucketer` object creates binning of the values of `x` into equally sized bins. The attributes `counts`, the number of elements per bin, and `boundaries`, the actual boundaries that resulted from the binning strategy, are assigned to the object instance. In this example we choose to get 4 bins:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "counts [891 109 110 890]\n", - "boundaries [4.53978687e-05 2.50022585e-01 4.99999772e-01 7.49976959e-01\n", - " 9.99954146e-01]\n" - ] - } - ], - "source": [ - "mySimpleBucketer = SimpleBucketer(bin_count=4)\n", - "mySimpleBucketer.fit(x)\n", - "print(\"counts\", mySimpleBucketer.counts_)\n", - "print(\"boundaries\", mySimpleBucketer.boundaries_)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAD4CAYAAAD8Zh1EAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy86wFpkAAAACXBIWXMAAAsTAAALEwEAmpwYAAAfF0lEQVR4nO3de3xU9Z3/8dcnkwsh3CGAXANysdBFxYi32npBBbXQixesrbY/V35t112r9relq7Vdu93ftv52W926tVpt0Xq31lKLIEW8FwFvlItIuAeQAIGEhJDLzOf3xxzoGBNIYDJnZvJ+Ph7jnPnOycybM5O3J+ecmWPujoiIZL6csAOIiEhyqNBFRLKECl1EJEuo0EVEsoQKXUQkS+SG9cT9+vXzkpKSsJ5eRCQjvfXWW7vcvbil+0Ir9JKSEpYtWxbW04uIZCQz29TafdrkIiKSJVToIiJZQoUuIpIlVOgiIllChS4ikiWOWOhm9qCZVZjZilbuNzO728zKzGy5mU1MfkwRETmStqyh/waYcpj7pwKjg8tM4BfHHktERNrriMehu/srZlZymFmmAw95/Ht4F5tZLzM7zt23JyukiGQnd4fGRmINjXhDPd7QgNfXE6uvxxsaIdqER2OHrj3aBLEYHo1CNJpwHYNYFG+Kxq+jMfAYuMefwx2c+DUHrz3h/mCstfEWf6bZ/X/7R7X0L/3IrW7nnkvh3/1dEpdkXDI+WDQY2JJwuzwY+1ihm9lM4mvxDBs2LAlPLSJh8ViMaFUV0cpKopWVNFXuIbqnkuiePUT31RCrrSVWE1zX1hKtDab378fr48XtDQ2tFGAWMjs0mdu/f9oWepu5+33AfQClpaWd5FUUyUzuTtPOnTSsW0fDps00bttG4/bt8ett22iqqIBotMWftS5dyOnWjZyiruQUFREp6kZe/wHxsa5dsYICcgrysfx8LL8guM7HCvLJyc/HCoKxSAQiuVgkB3IiWG4EcnKw3NxD15aTA5FIMG9wnZMDZphZvEgTLwcztnifBVct/5zR8n2W8LhhSkahbwWGJtweEoyJSIbwpibq166l7r3lHFi5gvqyddSvW0esuvpvM+XmkjdgAHmDBlE0aRK5AweS27cvkT59yO3Tm0ifPkR69yG3dy8sPz+8f0wnloxCnwPcYGaPA6cBVdp+LpLevKmJAytWUPPGG+z/y2LqVqzA6+oAiPTqRcHo0fS45GIKjh9FwajjyS8pIbe4OL72K2nriIVuZo8B5wD9zKwc+D6QB+Du9wJzgYuBMmA/8LWOCisiRy924AA1r7zCvnnzqHnt9fjatxldxo2j12WXUThhAoUnTiBv6NC02YQg7dOWo1yuOsL9DvxD0hKJSNK4O3XLlrHnqaeo+fNCYvv3E+nTh+6TJ9PtU2fR9YwzyO3dO+yYkiShfX2uiHScWF0de3//e/Y+9hj1a8vI6dGDHpdcTI+pU+k6aVJ8p6JkHb2qIlkktn8/ex5/gt0PPEB09266jB/PcT/6ET0unkpOYWHY8aSDqdBFsoDHYlQ98wwVP/0Z0d27KTrzDPp985t0LS0NO5qkkApdJMPVvfceH97xQw6sXEnhxIn0/++76TpRX6nUGanQRTJUrKGBXXffze4HHiS3uJhBd95Jj0sv0REqnZgKXSQD1a9dy9Zbvk39Bx/Q6/LL6f+d7xDpVhR2LAmZCl0kw1TPm8e2f7mVnK5dGXLvL+h+zjlhR5I0oUIXyRAei7Hzpz9j9/33U3jSSQy+6y7yBvQPO5akERW6SAbwxka2ffdfqH7uOXpdcQUDb7tV35ciH6NCF0lzsbo6ym+8kdpXXqX45pvpe/3fa8entEiFLpLGYvX1bPnGN9m/ZAkD7/hXel9xRdiRJI2p0EXSlDc0sPWfbmT/m28y6D/+Lz2nTw87kqS5tpxTVERSzGMxts2aRc3LLzPw+99XmUubqNBF0tDOu++meu7z9P/2LfSecWXYcSRDqNBF0kzVnDnsvveX9Lr8cvpcd13YcSSDqNBF0kjd8uVsv/U2uk6axMDbv6ejWaRdVOgiaSJaVcXWb91Ebv/+DL7rZ1heXtiRJMPoKBeRNODubL/tezRWVFDy6CM6i5AcFa2hi6SBPY89xr4FC+h/000UTpgQdhzJUCp0kZA1bNpExU/upOjss+nzta+GHUcymApdJEQei7H9tu9heXkc928/xHL0KylHT+8ekRDtfeIJ9i9dyoBZ3yFvwICw40iGU6GLhKRx+3Yq7vx/FJ15Jj2/8IWw40gWUKGLhKTizjvxWIyBd9yh480lKVToIiGoXbKE6rnP0/f6vyd/yOCw40iWUKGLpJg3NbHjR/9O3qBB9NVH+yWJ9MEikRTb8+ST1K9Zw+C77yKnS5ew40gW0Rq6SApFa2rZ9fN76HraaXS/4IKw40iWUaGLpFDlQ7OJVlbS/5abtSNUkk6FLpIiTXv2UPngr+k2+Xx9vF86hApdJEV2/+pXxGpr6X/jjWFHkSzVpkI3sylmtsbMysxsVgv3DzOzRWb2jpktN7OLkx9VJHM1VlSw57eP0HPaZykYPTrsOJKljljoZhYB7gGmAuOAq8xsXLPZbgOedPeTgRnA/yQ7qEgmq5w9G29spN83vxl2FMlibVlDnwSUuft6d28AHgean7HWgR7BdE9gW/IiimS2aFUVex97nB5Tp5I/fHjYcSSLtaXQBwNbEm6XB2OJfgB82czKgbnAP7b0QGY208yWmdmynTt3HkVckcxT+cgjxPbvp+/M68OOIlkuWTtFrwJ+4+5DgIuBh83sY4/t7ve5e6m7lxYXFyfpqUXSV6y2lj2zH6LbuefSZezYsONIlmtLoW8FhibcHhKMJboOeBLA3f8CdAH6JSOgSCbb89RTRKuqtHYuKdGWQl8KjDazEWaWT3yn55xm82wGzgcws08QL3RtU5FOzZuaqHzoIbqWltL15JPDjiOdwBEL3d2bgBuA+cBq4kezrDSzO8xsWjDbLcD1ZvYe8BjwVXf3jgotkgn2vfgiTdu20+er14YdRTqJNn05l7vPJb6zM3Hs9oTpVcBZyY0mktn2PPQweYMH0+3cc8OOIp2EPikq0gEOrF7N/mXL6H311VgkEnYc6SRU6CIdoPK3v8UKC+n1RZ1aTlJHhS6SZE2VlVT/8Tl6Tp9GpGfPsONIJ6JCF0myhf89C29ooMdVM8KOIp2MCl0kiWIe44Fh63n+quMpGntC2HGkk1GhiyTRG9veYHXODsZdc0PYUaQTUqGLJNHTHzxN74LenDfsvLCjSCekQhdJkl11u3h5y8tMO34a+ZH8sONIJ6RCF0mSP677I03exBfG6FBFCYcKXSQJ3J1ny57lpOKTGNlzZNhxpJNSoYskwfJdy1lftZ7Pjfpc2FGkE1OhiyTBH8r+QJdIFy4quSjsKNKJqdBFjlF9tJ55G+YxefhkuuV3CzuOdGIqdJFj9NKWl9jXuI9px0874rwiHUmFLnKMnlv3HP0L+zNp4KSwo0gnp0IXOQZ7Duzhta2vccnIS4jk6GtyJVwqdJFjMH/jfJq8iUtGXhJ2FBEVusixeG79c4zqNYqxfcaGHUVEhS5ytMr3lfPezve0di5pQ4UucpTmbZwHwNQRU0NOIhKnQhc5SnM3zOXE4hMZ3G1w2FFEABW6yFFZt3cda/es1dq5pBUVushRmLdxHjmWo4/6S1pRoYu0k7szf+N8ThlwCv0K+4UdR+QQFbpIO63du5YNVRu4aLjWziW9qNBF2mn+xvnkWA6Th08OO4rIR6jQRdrB3Xlh4wucOuBU+hb2DTuOyEeo0EXaYe3etWys3siFJReGHUXkY1ToIu2wYNMCciyH84adF3YUkY9RoYu0w4KNC5jYf6KObpG01KZCN7MpZrbGzMrMbFYr81xhZqvMbKWZPZrcmCLhW793Peuq1mlnqKSt3CPNYGYR4B7gAqAcWGpmc9x9VcI8o4HvAme5+x4z699RgUXCsmDTAgAmD1OhS3pqyxr6JKDM3de7ewPwODC92TzXA/e4+x4Ad69IbkyR8C3cvJATi09kQNGAsKOItKgthT4Y2JJwuzwYSzQGGGNmr5vZYjOb0tIDmdlMM1tmZst27tx5dIlFQlC+r5zVlau1di5pLVk7RXOB0cA5wFXA/WbWq/lM7n6fu5e6e2lxcXGSnlqk4y3cvBCA84efH3ISkda1pdC3AkMTbg8JxhKVA3PcvdHdNwAfEC94kaywcPNCxvYey9DuQ488s0hI2lLoS4HRZjbCzPKBGcCcZvM8S3ztHDPrR3wTzPrkxRQJz666Xbxb8S7nD9PauaS3Ixa6uzcBNwDzgdXAk+6+0szuMLNpwWzzgd1mtgpYBPwfd9/dUaFFUmnRlkU4rg8TSdo74mGLAO4+F5jbbOz2hGkHbg4uIlll4eaFDOk2hDG9x4QdReSw9ElRkcOoaajhze1vct6w8zCzsOOIHJYKXeQwXtv6Gk2xJm0/l4ygQhc5jBc3v0ifLn04sfjEsKOIHJEKXaQVjdFGXt36KucMPYdITiTsOCJHpEIXacWSD5dQ01jDeUN1dItkBhW6SCte3PwihbmFnHbcaWFHEWkTFbpIC2Ie46UtL3HWoLPoktsl7DgibaJCF2nBqt2rqKir4Nxh54YdRaTNVOgiLXhx84tELMKnB3867CgibaZCF2nBoi2LmDhgIr269Ao7ikibqdBFmtlSvYWyvWWcO1SbWySzqNBFmlm0ZREA5ww9J9wgIu2kQhdp5qXylxjVa5S++1wyjgpdJMHeqs28veMtbW6RjKRCF0mwbeXvGFpfz3mFQ8KOItJubfo+dJHOYtyWt/ljleNjpocdRaTdtIYuclDjAShbCGOnYhF9GZdkHhW6yEEbXoHGWjjhkrCTiBwVFbrIQWv+BPndYIQ+HSqZSYUuAhCLwZrnYdT5kFsQdhqRo6JCFwHY+hbU7ICx2twimUuFLgLxzS0WgTEXhp1E5Kip0EUA3p8LJWdBYe+wk4gcNRW6yK4y2LUGTrg07CQix0SFLvL+c/HrsVPDzSFyjFToIu//CQZOgF7Dwk4ickxU6NK57dsB5Uu1uUWyggpdOrc1cwHXp0MlK6jQpXN7/0/QuwQGjA87icgxU6FL53WgGja8HN/cYhZ2GpFjpkKXzmvtCxBt0PZzyRptKnQzm2Jma8yszMxmHWa+L5qZm1lp8iKKdJD3n4OiYhg6KewkIklxxEI3swhwDzAVGAdcZWbjWpivO3Aj8GayQ4okXeMBWLsgvjM0R999LtmhLWvok4Ayd1/v7g3A40BLp3P5IfBj4EAS84l0jPWLoKEGTvhs2ElEkqYthT4Y2JJwuzwYO8TMJgJD3f1Ph3sgM5tpZsvMbNnOnTvbHVYkaVb/EQp66rvPJasc805RM8sB/gu45Ujzuvt97l7q7qXFxcXH+tQiRyfaGD9ccewUyM0PO41I0rSl0LcCQxNuDwnGDuoOfBJ4ycw2AqcDc7RjVNLWxlfhwF74xLSwk4gkVVsKfSkw2sxGmFk+MAOYc/BOd69y937uXuLuJcBiYJq7L+uQxCLHatUcyCuKn51IJIscsdDdvQm4AZgPrAaedPeVZnaHmWkVRzJLLBo/XHHMhZBXGHYakaTKbctM7j4XmNts7PZW5j3n2GOJdJBNr0PtThjX0oFaIplNnxSVzmXls5BbCKN1qjnJPip06Txi0fjhimMuhPyisNOIJJ0KXTqPTa9DbQWM/3zYSUQ6hApdOo+Vz0JeV21ukaylQpfOIdoEq/4AYy7S5hbJWip06Rw2vgL7d8Envxh2EpEOo0KXzuGvv4P87jDqgrCTiHQYFbpkv6b6+NEtn7gU8rqEnUakw6jQJfutXQD1VfDJy8JOItKhVOiS/f76FHTtByPPCTuJSIdSoUt2O1ANH8yLH3seadM3XYhkLBW6ZLfVc6DpAEy4MuwkIh1OhS7Z7b3Hoc9IGKKv55fsp0KX7LV3C2x8DSbMALOw04h0OBW6ZK/lTwAOJ2pzi3QOKnTJTu7w3mMw/CzoXRJ2GpGUUKFLdtqyBHaXwYlXhZ1EJGVU6JKd3v1t/Lyh4z8XdhKRlFGhS/apr4EVz8SPPS/oHnYakZRRoUv2WfUsNNTAxK+EnUQkpVTokn3emg39xsDQ08JOIpJSKnTJLjtWQvkSmHitjj2XTkeFLlmlYtG9eKQATvpS2FFEUk6FLlmj5kAjm1Yv453un4GufcKOI5JyKnTJGs+8s5XL628lZ9pdYUcRCYUKXbKCuzP7jY2cOKQXJ40cFHYckVCo0CUrvFa2i3U7a7n2zJKwo4iERoUuWWH2GxvpW5TPJROOCzuKSGhU6JLx1u+sYeH7FXzptGEU5EbCjiMSGhW6ZLz7X91AXiSHa84oCTuKSKjaVOhmNsXM1phZmZnNauH+m81slZktN7OFZjY8+VFFPq5i3wF+93Y5l50yhOLuBWHHEQnVEQvdzCLAPcBUYBxwlZmNazbbO0Cpu08AngZ+kuygIi2Z/cZGGqMxrj97ZNhRRELXljX0SUCZu6939wbgcWB64gzuvsjd9wc3FwNDkhtT5ONq6pt4+C+bmDJ+ICP6FYUdRyR0bSn0wcCWhNvlwVhrrgOeb+kOM5tpZsvMbNnOnTvbnlKkBY++uYnqA03M/LTWzkUgyTtFzezLQClwZ0v3u/t97l7q7qXFxcXJfGrpZGrrm7j35fWcPbofJw/rHXYckbSQ24Z5tgJDE24PCcY+wswmA7cCn3H3+uTEE2nZ7L9spLK2gZsuGBN2FJG00ZY19KXAaDMbYWb5wAxgTuIMZnYy8EtgmrtXJD+myN/sO9DIfa+s59yxxUzU2rnIIUcsdHdvAm4A5gOrgSfdfaWZ3WFm04LZ7gS6AU+Z2btmNqeVhxM5ZrPf2Mje/Y18a7LWzkUStWWTC+4+F5jbbOz2hOnJSc4l0qLdNfX88uX1TP5Ef04c2ivsOCJpRZ8UlYxy18K17G+MMmvqCWFHEUk7KnTJGGUV+3jkzc1cfdowRvXvHnYckbSjQpeM8e9z36drfoQbzx8ddhSRtKRCl4ywaE0FL75fwT+eN4q+3fSdLSItUaFL2qtriPK9Z1dwfHGRTmAhchhtOspFJEx3LVxL+Z46nph5ur7vXOQwtIYuaW319mp+9ep6rigdwmkj+4YdRyStqdAlbTU0xbjlyffoWZjHd6d+Iuw4ImlPm1wkbf30zx+wans1919TSu+i/LDjiKQ9raFLWlqyoZJ7X17HjFOHcsG4AWHHEckIKnRJO5W1Ddz0xLsM7d2V713a/ORYItIabXKRtBKNOf/02DvsrKnn6a+fQVGB3qIibaU1dEkr//nCGl4r28UPp49nwpBeYccRySgqdEkbv3+nnP95Kb7d/MpTh4UdRyTjqNAlLbxetot/fno5p4/sw79OHx92HJGMpEKX0K3YWsXXH36LEf2K+OVXSvVpUJGjpEKXUK3cVsWXH3iT7l1y+c3XJtGzMC/sSCIZS4UuoVm5rYqrf/UmXfMiPD7zDAb1Kgw7kkhGU6FLKN4o28WM+xYfKvNhfbuGHUkk46nQJeWeebuca3+9hON6duGpb5ypMhdJEn1qQ1KmMRrjJ/Pe5/5XN3DGyL7c+5VTtM1cJIlU6JIS2/bWccOjb/P25r1cc8ZwbrtkHPm5+gNRJJlU6NKh3J2n3irn355bRczh5186mUsnDAo7lkhWUqFLh9mwq5bb/7CCV9fuYlJJH35y2QRK+hWFHUska6nQJekqaxu4e+Fafrt4EwW5Ofxw+niuPm04OTkWdjSRrKZCl6TZVVPPr1/fwENvbKK2oYkrTx3GTReMpn/3LmFHE+kUVOhyzFZtq+bRJZt4alk5DdEYU8YP5KYLxjBmQPewo4l0Kip0OSq7a+qZt/JDnli6heXlVeRHcvj8yYP5358ZycjibmHHE+mUVOjSJu7O5sr9LFxdwfyVH7J0YyUxhxMGduf7nx3H504arPN+ioRMhS4tisacDbtqeHvzXhav283i9bvZVnUAgLEDunPDuaO4cPxAxg/qgZl2doqkAxV6J+fu7KiuZ+PuWjbsqmX19mpWbqtm1bZq6hqjAPQtyuf0kX35xvF9OXtUPx16KJKm2lToZjYFuAuIAL9y9/9odn8B8BBwCrAbuNLdNyY3qrRXYzRGdV0ju2oa2FF9gB3VB6jYV39oetPu/Wzavf9QcQMU5UcYP6gnV546lE8O7smEIT0Z3b+b1sJFMsARC93MIsA9wAVAObDUzOa4+6qE2a4D9rj7KDObAfwYuLIjAmcid6cp5kRjTmM0RjTWttuNTTEONMWoa4hS3xSlriFKXWOUA40x6hqj1DfGb+9viFJV10hVXSPVwaWqrpHahmiLeXoW5tG/ewFD+3TlzOP7MaJfV0r6FVHSt4jBvQp1vLhIhmrLGvokoMzd1wOY2ePAdCCx0KcDPwimnwZ+bmbm7p7ErAA8uXQLv3xlHQAe/MeJl+bBJ3MHx+PXCQkOznPw/r/Ne3C+5mPNHvPgbSdhvPXHxCHq8aLuCAW5ORTmRyjMi9CzMI8ehXkM6d2VnoPy6Fl48JJLv+4FDOjRhQHdu9C/RwFd8nRGIJFs1JZCHwxsSbhdDpzW2jzu3mRmVUBfYFfiTGY2E5gJMGzY0Z0EuHdRPicM7AHBSqTFHze4PjR8aAyDYOrQ/dZ8LJjxoz8fn6f5Y9LSzx96HDs078Hnzc0xIjlGXsSI5OS0eDs3Eh/LzclJuM/Ii+TQJS+HLnnx0k68LsjN0Zq0iHxESneKuvt9wH0ApaWlR7XaesG4AVwwbkBSc4mIZIO2fH/pVmBowu0hwViL85hZLtCT+M5RERFJkbYU+lJgtJmNMLN8YAYwp9k8c4Brg+nLgBc7Yvu5iIi07oibXIJt4jcA84kftvigu680szuAZe4+B3gAeNjMyoBK4qUvIiIp1KZt6O4+F5jbbOz2hOkDwOXJjSYiIu2hc4CJiGQJFbqISJZQoYuIZAkVuohIlrCwji40s53ApqP88X40+xRqmlCu9knXXJC+2ZSrfbIx13B3L27pjtAK/ViY2TJ3Lw07R3PK1T7pmgvSN5tytU9ny6VNLiIiWUKFLiKSJTK10O8LO0ArlKt90jUXpG825WqfTpUrI7ehi4jIx2XqGrqIiDSjQhcRyRIZV+hmNsXM1phZmZnNSvFzDzWzRWa2ysxWmtmNwfgPzGyrmb0bXC5O+JnvBlnXmNlFHZhto5n9NXj+ZcFYHzNbYGZrg+vewbiZ2d1BruVmNrGDMo1NWCbvmlm1mX0rjOVlZg+aWYWZrUgYa/fyMbNrg/nXmtm1LT1XEnLdaWbvB8/9ezPrFYyXmFldwnK7N+FnTgle/7Ig+zGdzqqVXO1+3ZL9+9pKricSMm00s3eD8VQur9a6IbXvMXfPmAvxr+9dB4wE8oH3gHEpfP7jgInBdHfgA2Ac8fOpfruF+ccFGQuAEUH2SAdl2wj0azb2E2BWMD0L+HEwfTHwPPGz550OvJmi1+5DYHgYywv4NDARWHG0ywfoA6wPrnsH0707INeFQG4w/eOEXCWJ8zV7nCVBVguyT+2AXO163Tri97WlXM3u/0/g9hCWV2vdkNL3WKatoR86YbW7NwAHT1idEu6+3d3fDqb3AauJn0+1NdOBx9293t03AGXE/w2pMh2YHUzPBj6XMP6Qxy0GepnZcR2c5Xxgnbsf7tPBHba83P0V4t/V3/z52rN8LgIWuHulu+8BFgBTkp3L3V9w96bg5mLiZwlrVZCth7sv9ngrPJTwb0larsNo7XVL+u/r4XIFa9lXAI8d7jE6aHm11g0pfY9lWqG3dMLqwxVqhzGzEuBk4M1g6IbgT6cHD/5ZRWrzOvCCmb1l8ZNxAwxw9+3B9IfAwZOxhrEcZ/DRX7Swlxe0f/mEsdz+F/E1uYNGmNk7ZvaymZ0djA0OsqQiV3tet1Qvr7OBHe6+NmEs5curWTek9D2WaYWeFsysG/A74FvuXg38AjgeOAnYTvzPvlT7lLtPBKYC/2Bmn068M1gTCeUYVYufunAa8FQwlA7L6yPCXD6tMbNbgSbgkWBoOzDM3U8GbgYeNbMeKYyUdq9bM1fx0ZWGlC+vFrrhkFS8xzKt0NtywuoOZWZ5xF+wR9z9GQB33+HuUXePAffzt80EKcvr7luD6wrg90GGHQc3pQTXFanOFZgKvO3uO4KMoS+vQHuXT8rymdlXgUuBq4MiINiksTuYfov49ukxQYbEzTIdkusoXrdULq9c4AvAEwl5U7q8WuoGUvwey7RCb8sJqztMsI3uAWC1u/9Xwnji9ufPAwf3wM8BZphZgZmNAEYT3xmT7FxFZtb94DTxnWor+OjJu68F/pCQ65pgT/vpQFXCn4Ud4SNrTmEvrwTtXT7zgQvNrHewueHCYCypzGwK8M/ANHffnzBebGaRYHok8eWzPshWbWanB+/RaxL+LcnM1d7XLZW/r5OB99390KaUVC6v1rqBVL/HjmXPbhgX4nuHPyD+f9tbU/zcnyL+J9Ny4N3gcjHwMPDXYHwOcFzCz9waZF3DMe5JP0yukcSPIHgPWHlwuQB9gYXAWuDPQJ9g3IB7glx/BUo7cJkVAbuBngljKV9exP+Hsh1oJL5d8rqjWT7Et2mXBZevdVCuMuLbUQ++x+4N5v1i8Pq+C7wNfDbhcUqJF+w64OcEnwJPcq52v27J/n1tKVcw/hvg683mTeXyaq0bUvoe00f/RUSyRKZtchERkVao0EVEsoQKXUQkS6jQRUSyhApdRCRLqNBFRLKECl1EJEv8f/DTtEYhCV2OAAAAAElFTkSuQmCC\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "df = pd.DataFrame({\"x\": x})\n", - "df[\"label\"] = pd.cut(x, bins=mySimpleBucketer.boundaries_, include_lowest=True)\n", - "\n", - "fig, ax = plt.subplots()\n", - "for label in df.label.unique():\n", - " df[df.label == label].plot(ax=ax, y=\"x\", legend=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As can be seen, the number of elements in the tails of the data is larger than in the middle:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "df.groupby(\"label\")[\"x\"].count().plot(kind=\"bar\")\n", - "plt.title(\"Histogram\");" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Quantile binning " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `QuantileBucketer` object creates bins that all contain an equal amount of samples" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "counts [500 500 500 500]\n", - "boundaries [4.53978687e-05 6.67631251e-03 4.98750010e-01 9.93257042e-01\n", - " 9.99954146e-01]\n" - ] - } - ], - "source": [ - "myQuantileBucketer = QuantileBucketer(bin_count=4)\n", - "myQuantileBucketer.fit(x)\n", - "print(\"counts\", myQuantileBucketer.counts_)\n", - "print(\"boundaries\", myQuantileBucketer.boundaries_)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "df = pd.DataFrame({\"x\": x})\n", - "df[\"label\"] = pd.cut(x, bins=myQuantileBucketer.boundaries_, include_lowest=True)\n", - "\n", - "fig, ax = plt.subplots()\n", - "for label in df.label.unique():\n", - " df[df.label == label].plot(ax=ax, y=\"x\", legend=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As can be seen, the number of elements is the same in all bins:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "df.groupby(\"label\")[\"x\"].count().plot(kind=\"bar\")\n", - "plt.title(\"Histogram\");" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Binning by agglomerative clustering" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `AgglomerativeBucketer` class applies the Scikit-Learn `AgglomerativeClustering` algorithm to the data and uses the clusters to determine the bins.\n", - "We use different data to show the value of this algoritm; we create the following distribution:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "x_agglomerative = np.append(np.random.normal(0, 1, size=1000), np.random.normal(6, 0.2, size=50))\n", - "plt.hist(x_agglomerative, bins=20)\n", - "plt.title(\"Sample data\");" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When we apply the `AgglomerativeBucketer` algorithm with 2 bins, we see that the algorithm nicely creates a split in between the two centers" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "counts [1000 50]\n", - "boundaries [-2.71525699097944, 4.582406874429196, 6.454492599188006]\n" - ] - } - ], - "source": [ - "myAgglomerativeBucketer = AgglomerativeBucketer(bin_count=2)\n", - "myAgglomerativeBucketer.fit(x_agglomerative)\n", - "print(\"counts\", myAgglomerativeBucketer.counts_)\n", - "print(\"boundaries\", myAgglomerativeBucketer.boundaries_)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "df = pd.DataFrame({\"x\": x_agglomerative})\n", - "df[\"label\"] = pd.cut(x_agglomerative, bins=myAgglomerativeBucketer.boundaries_, include_lowest=True)\n", - "\n", - "fig, ax = plt.subplots()\n", - "for label in df.label.unique():\n", - " df[df.label == label].hist(ax=ax)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that the `SimpleBucketer` strategy would just have created a split in the middle of the maximum and the minimum (at about 1.75). The `QuantileBucketer` strategy had created two bins with equal amount of elements in it, resulting in a split at around 0." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "counts_agglomerative_simple, boundaries_agglomerative_simple = SimpleBucketer.simple_bins(x_agglomerative, 2)\n", - "\n", - "df = pd.DataFrame({\"x\": x_agglomerative})\n", - "df[\"label\"] = pd.cut(x_agglomerative, bins=boundaries_agglomerative_simple, include_lowest=True)\n", - "\n", - "fig, ax = plt.subplots()\n", - "for label in df.label.unique():\n", - " df[df.label == label].hist(ax=ax)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "counts_agglomerative_quantile, boundaries_agglomerative_quantile = QuantileBucketer.quantile_bins(x_agglomerative, 2)\n", - "\n", - "df = pd.DataFrame({\"x\": x_agglomerative})\n", - "df[\"label\"] = pd.cut(x_agglomerative, bins=boundaries_agglomerative_quantile, include_lowest=True)\n", - "\n", - "fig, ax = plt.subplots()\n", - "for label in df.label.unique():\n", - " df[df.label == label].hist(ax=ax)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Binning with Decision Trees\n", - "\n", - "Binning with decision trees leverages the information of a binary feature or the binary target in order to create buckets that have a significantly different proportion of the binary feature/target.
\n", - "\n", - "It works by fitting a tree on 1 feature only.
\n", - "It leverages the properties of the split finder algorithm in the decision tree. The splits are done to maximize the gini/entropy.
\n", - "The leaves approximate the optimal bins.\n", - "\n", - "The example below shows a distribution defined by a step function" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY0AAAD4CAYAAAAQP7oXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy86wFpkAAAACXBIWXMAAAsTAAALEwEAmpwYAAAWNUlEQVR4nO3df5ClVX3n8fdnpkERlJ9K6czsgsWoYdUEdkQMVVlLNAVGGavWuFibhFhTmaotUROsRMxGzbp/qDGrwQrrOhEUs6zIorV2uZMYC3AttwLFCK7KENZxjDIDykiAKEiGHr/7x32GvQ7T3U/3dN97bvf7VdU1z49zz3MudM1nzjnPc55UFZIk9bFm3A2QJE0OQ0OS1JuhIUnqzdCQJPVmaEiSepsadwMA1qxZU8ccc8y4myFJE+XRRx+tqhrpP/6bCI1jjjmGRx55ZNzNkKSJkuSno76mw1OSpN4MDUlSb4aGJKk3Q0OS1JuhIUnqzdCQpBUqydVJ7k/yrVnOJ8lHkuxK8o0kZ89Xp6EhSSvXJ4EL5jh/IbCx+9kKfHS+Cpt4TkOSxmXfvn187GMfY//+/WO5/mtf+1pe8pKXLEvdVfWVJKfNUWQz8KkavCPjliQnJHl2Vd032wcMDUmr2mc/+1ne9a53AZBk5Nd/znOecyShMZVkx9D+tqratoDPrwPuGdrf0x1buaGxe99Pxt0ELdBzn3ncuJsgPeFgD+OBBx7gpJNOGnNrFmymqjaN8oLOaUha1Q4cOADA2rVrx9ySsdgLbBjaX98dm9XE9zQk6UgcNjR+tGt0DTjljNFd68mmgUuTXAe8FHh4rvkMMDQkrXIruaeR5NPAy4FTkuwB3gMcBVBV/wXYDrwa2AU8CrxpvjoNDUmr2szMDLAyQ6Oq3jjP+QLevJA6ndOQtKqt5J7GcjA0JK1qB0NjzRr/OuzD/0qSVrUDBw6wZs2asTyjMYmc05DUhE984hPcfPPNI7/u7bff7tDUAhgakprwvve9j3vvvZdnPetZI7/2a17zmpFfc1IZGpKaUFVs3ryZa6+9dtxN0RwMDUlNGNz9OYdRPnCnWTkRLknqzdCQ1AzvYGqfoSGpCfMOT6kJhoYkqTdDQ1IzHJ5qn6EhqQkOT02GXqGR5PeS3JnkW0k+neSpSU5PcmuSXUk+k+ToruxTuv1d3fnTlvUbSJJGZt7QSLIOeCuwqapeCKwFLgY+AHy4qs4AHgS2dB/ZAjzYHf9wV06S5uXwVPv6Dk9NAcckmQKexuCl468AbujOXwO8rtve3O3TnT8//iZImofDU5Nh3tCoqr3AnwLfZxAWDwNfAx6qqpmu2B5gXbe9Drin++xMV/7kQ+tNsjXJjiQ7Dr4ERZLUtj7DUycy6D2cDjwHOBa44EgvXFXbqmpTVW2amnI1E0kOT02CPsNTrwS+W1X7qupx4HPAecAJ3XAVwHpgb7e9F9gA0J0/HnhgSVstacVxeGoy9AmN7wPnJnlaNzdxPrATuBl4fVfmEuDz3fZ0t093/qbyt0GSVoQ+cxq3MpjQvh34ZveZbcA7gMuS7GIwZ3FV95GrgJO745cBly9DuyWtQA5Pta/XZEJVvQd4zyGHdwPnHKbsY8CvH3nTJK0mDkhMBp8Il9QMexrtMzQkSb0ZGpKa4PDUZDA0JDXD4an2GRqSmmBPYzIYGpKk3ly/QyO3e99PRnat5z7zuJFdS0fO4an22dOQ1ASHpyaDoSFJ6s3QkNQMh6eWXpILktzdvU31Scs6JflnSW5OckeSbyR59Vz1GRqSmuDw1NJLsha4ErgQOBN4Y5IzDyn2R8D1VXUWg7ey/ue56jQ0JGnlOgfYVVW7q2o/cB2D9yMNK+AZ3fbxwL1zVejdU5Ka4fDUgk0l2TG0v62qtg3tP/Em1c4e4KWH1PHHwN8keQuDl+y9cs4LLr6tkrR0HJ5alJmq2nSEdbwR+GRV/ackLwP+MskLq+pnhyvs8JQkrVxPvEm1M/yW1YO2ANcDVNXfAk8FTpmtQkNDUjMcnlpytwEbk5ye5GgGE93Th5T5PoM3spLkFxiExr7ZKjQ0JDXB4amlV1UzwKXAF4G7GNwldWeS9ya5qCv2duB3kvwf4NPAb8/1im7nNCRpBauq7cD2Q469e2h7J3Be3/rsaUhqhsNT7TM0JDXB4anJYGhIaoY9jfYZGpKaYE9jMhgakqTeDA1JzXB4qn2GhqQmODw1GQwNSVJvhoakZjg81T5DQ1ITHJ6aDIaGJKk3Q0NSMxyeap8LFmpF273vJyO71nOfedzIrrUSOTw1GexpSJJ6MzQkNcPhqfYZGpKa4PDUZDA0JEm9GRqSmuHwVPsMDUlNcHhqMhgakpphT6N9vUIjyQlJbkjyd0nuSvKyJCcl+VKSb3d/ntiVTZKPJNmV5BtJzl7eryBJGpW+PY0rgL+uqhcAvwjcBVwO3FhVG4Ebu32AC4GN3c9W4KNL2mJJK5LDU5Nh3tBIcjzwK8BVAFW1v6oeAjYD13TFrgFe121vBj5VA7cAJyR59hK3W9IK5PBU+/r0NE4H9gGfSHJHko8nORY4taru68r8ADi1214H3DP0+T3dsZ+TZGuSHUl2zMzMLP4bSFoR7GlMhj6hMQWcDXy0qs4CHuH/D0UBUIP/2wv6P15V26pqU1VtmppyCSxJmgR9QmMPsKeqbu32b2AQIj88OOzU/Xl/d34vsGHo8+u7Y5I0J4en2jdvaFTVD4B7kjy/O3Q+sBOYBi7pjl0CfL7bngZ+q7uL6lzg4aFhLEk6LIenJkPfcaG3ANcmORrYDbyJQeBcn2QL8D3gDV3Z7cCrgV3Ao11ZSdIK0Cs0qurrwKbDnDr/MGULePORNUuaPKN8dweszPd3ODzVPp8Il9QEh6cmg6EhSStYkguS3N2t0nH5LGXekGRnkjuT/Le56vNeV0nNcHhqaSVZC1wJvIrBnbC3JZmuqp1DZTYC7wTOq6oHkzxrrjrtaUhqgsNTy+IcYFdV7a6q/cB1DFbtGPY7wJVV9SBAVd3PHAwNSZpcUwdX1uh+th5yvs8KHc8Dnpfkfye5JckFc17wyNssSUvD4akFm6mqw93ZuhBTDBaYfTmDh7G/kuRF3RqDT2JPQ1ITHJ5aFn1W6NgDTFfV41X1XeD/MgiRwzI0JGnlug3YmOT07uHsixms2jHsfzDoZZDkFAbDVbtnq9DhKUmL96NdS1dXFXns4aWtc5WrqpkklwJfBNYCV1fVnUneC+yoqunu3K8m2QkcAH6/qh6YrU5DQ1ITamELZaunqtrOYHmn4WPvHtou4LLuZ14OT0lqhvPg7TM0JEm9GRqSmuDdU5PB0JDUDJ/TaJ+hIakJ9jQmg6EhSerN0JDUDIen2mdoSGqCw1OTwdCQJPVmaEhqRnB4qnWGhqQmODw1GQwNSVJvhoakZnj3VPtc5VZq0Fe/fBNf+qsvzFnmGcccNaLWzOGnDy9ZVY8/PrNkdWn5GBpSg/7iyiv426/+L44/4YRZy6xp4V/ldWDJqnrmKSdx9ov/xZLVp+VhaEgNOvCzA/zSv3wJ13/hS7OWee4zjxthi2bhC5NWHec0pAZ5J5FaZWhIjXJSWC0yNKQGVZWhoSY5pyG1qEdo7N73kxE1ppH5EzXBnobUIHsaapWhITXIiXC1yuEpqVWL7Gkc9dB3lrghcM9DS17lrDaceOzoLqYFs6chNcjhKbXK0JAaZGioVYaG1KCq8t0SapKhIUnqrXdoJFmb5I4kX+j2T09ya5JdST6T5Oju+FO6/V3d+dOWqe3SiuXwlFq1kJ7G24C7hvY/AHy4qs4AHgS2dMe3AA92xz/clZO0EIaGGtUrNJKsB34N+Hi3H+AVwA1dkWuA13Xbm7t9uvPnx99+aUEKQ0Nt6tvT+DPgD4CfdfsnAw9V1cG3puwB1nXb64B7ALrzD3flf06SrUl2JNkxM+PLV6RhVSz6OQ1pOc0bGkleA9xfVV9bygtX1baq2lRVm6amfMZQkpZDkguS3N3NM18+R7l/naSSbJqrvj5/W58HXJTk1cBTgWcAVwAnJJnqehPrgb1d+b3ABmBPkingeOCBHteR1HEiXEshyVrgSuBVDEaEbksyXVU7Dyn3dAbz1rfOV+e8PY2qemdVra+q04CLgZuq6t8CNwOv74pdAny+257u9unO31QupCMtiKGhJXIOsKuqdlfVfuA6BvPOh/qPDG5aemy+Co/kOY13AJcl2cVgzuKq7vhVwMnd8cuAWbtDkg5vEBrjboUmwNTBueHuZ+sh55+YY+4Mzz8DkORsYENV/c9eF1xI66rqy8CXu+3dDFLs0DKPAb++kHolPZk9DfUwU1VzzkHMJcka4EPAb/f9jE+ESw1yRFdL5OAc80HD888ATwdeCHw5yd8D5wLTc02GGxpSi5zT0NK4DdjYreBxNIN56emDJ6vq4ao6papO6+atbwEuqqods1Xova7SCCz4HRczj7Fm5qfL8m4MrR5VNZPkUuCLwFrg6qq6M8l7gR1VNT13DU9maEiNsqehpVBV24Hthxx79yxlXz5ffQ5PSQ0aLI0utcfQkBrkPLhaZWhIDXLBQrXK0JAa5BPhapWhITXK0FCLDA2pQVXl0uhqkqEhNcgnwtUqQ0NqkB0NtcrQkBrlnIZaZGhILaoiPt6nBhkaUoO85VatMjSkBhVOhKtNLlioiTDpdxMttP32NNQqQ0PN+4srr+D9/+GPxt2MkfulF79o3E2QnsTQUPO+8+27Oe64p7Pl371l3E1ZtDWP/cOCP3Phr56/DC2RjoyhoeZVFcc94xm89fffOe6mLJovU9JK4US4JKk3Q0PNc1JYaoehIUnqzdBQ8+xpSO0wNCRJvRkaap49DakdhoYkqTdDQ80bvFvCnobUAkNDE8FlwqU2GBpq34QvViitJIaGJoLDU1IbXHtKzVuuZdFdD0paOHsamgz2NKQmGBpq3qS/gElaSQwNTQTnNKTFSXJBkruT7Epy+WHOX5ZkZ5JvJLkxyT+fqz5DQ83zfdnS4iRZC1wJXAicCbwxyZmHFLsD2FRVLwZuAP5krjoNDU0EOxrSopwD7Kqq3VW1H7gO2DxcoKpurqpHu91bgPVzVThvaCTZkOTmrvtyZ5K3dcdPSvKlJN/u/jyxO54kH+m6Qt9IcvYivqj0BOc0pEVbB9wztL+nOzabLcBfzVVhn57GDPD2qjoTOBd4c9e9uRy4sao2Ajd2+zDoBm3sfrYCH+1xDWlOzmlIhzWVZMfQz9bFVpTkN4BNwAfnvOB8FVXVfcB93faPk9zFIKk2Ay/vil0DfBl4R3f8UzX45+EtSU5I8uyuHmlOh3t2Iv/0Y/Kzx32uQnqymaraNMf5vcCGof313bGfk+SVwL8H/lVV/dNcF1zQnEaS04CzgFuBU4eC4AfAqd12r+5Qkq0H03FmZmYhzdAq5NpT0qLcBmxMcnqSo4GLgenhAknOAj4GXFRV989XYe/QSHIc8Fngd6vqH4fPdb2KBQ08V9W2qtpUVZumpnwwXbNzTkNanKqaAS4FvgjcBVxfVXcmeW+Si7piHwSOA/57kq8nmZ6lOqDnMiJJjmIQGNdW1ee6wz88OOyU5NnAwYTq1R2SFsI5DWlxqmo7sP2QY+8e2n7lQurrc/dUgKuAu6rqQ0OnpoFLuu1LgM8PHf+t7i6qc4GHnc/QkSjKe26lRvTpaZwH/CbwzSRf7479IfB+4PokW4DvAW/ozm0HXg3sAh4F3rSUDZYkjU+fu6e+CrPOQp5/mPIFvPkI2yU9YfCO8HG3QhL4RLgmgPPgUjsMDU0EJ8KlNhgaap9dDakZhoYmgj0NqQ2Ghprnw31SOwwNTQR7GlIbDA01z56G1A5DQxPBBQulNhgaap49DakdhoYmglMaUhsMDTXPfobUDkNDE8G7p6Q2GBpqnnMaUjsMDU0EexpSGwwNNa/KlzBJrTA0JEm9GRpq3uAlTPY0pBYYGpKk3gwNta/KRUSkRhgakqTeDA01zzkNqR2GhiSpN0NDzRs8pmFPQ2qBoSFJ6s3QUPMK5zSkVhgakqTeDA01z7unpHYYGpKk3gwNNW/Q0xh3K6TJlOSCJHcn2ZXk8sOcf0qSz3Tnb01y2lz1TS1bSyVpEe558JGRXWvDiceO7FrjkGQtcCXwKmAPcFuS6araOVRsC/BgVZ2R5GLgA8C/ma3OiQ6Nq6++mvf/yQfH3QwtpQP7n3Roz957OfMXnj+GxkgT7xxgV1XtBkhyHbAZGA6NzcAfd9s3AH+eJDXLKzMnOjROPvlkznjeC8bdDC2hPP6TJx3beMZzueBVrxhDa6TmTSXZMbS/raq2De2vA+4Z2t8DvPSQOp4oU1UzSR4GTgZ+dNgLHnGTx2jz5s286JfPH3cztISOeug7426CNElmqmrTKC/oRLgkrVx7gQ1D++u7Y4ctk2QKOB54YLYKDQ1JWrluAzYmOT3J0cDFwPQhZaaBS7rt1wM3zTafARM+PCVJml03R3Ep8EVgLXB1Vd2Z5L3AjqqaBq4C/jLJLuAfGATLrDJHoIzMscceW488srjb7Hbve/LEqSaXcxoapSZuuT3ljEV/NMmjVTXSL+HwlCSpt2UJjfmeQJQkTaYlD42hJxAvBM4E3pjkzKW+jiRp9Jajp/HEE4hVtR84+ASiJGnCLcfdU32eQCTJVmBrt1tJfrrI600BM4v87KTyO68OfufV4Ui+8zFL2ZA+xnbLbfeo+7Z5C84jyY5RPxE5bn7n1cHvvDpM2ndejuGpPk8gSpIm0HKERp8nECVJE2jJh6dmewJxqa8z5IiHuCaQ33l18DuvDhP1nZt4IlySNBl8IlyS1JuhIUnqbaJDY7UtV5JkQ5Kbk+xMcmeSt427TaOQZG2SO5J8YdxtGYUkJyS5IcnfJbkrycvG3ablluT3ut/pbyX5dJKnjrtNSy3J1UnuT/KtoWMnJflSkm93f544zjb2MbGhsUqXK5kB3l5VZwLnAm9eBd8Z4G3AXeNuxAhdAfx1Vb0A+EVW+HdPsg54K7Cpql7I4AaaOZfnnlCfBC445NjlwI1VtRG4sdtv2sSGBqtwuZKquq+qbu+2f8zgL5N1423V8kqyHvg14OPjbssoJDke+BUG7zigqvZX1UNjbdRoTAHHdG+Oexpw75jbs+Sq6isM3lcxbDNwTbd9DfC6UbZpMSY5NA63XMmK/gt0WJLTgLOAW8fclOX2Z8AfAD8bcztG5XRgH/CJbkju40kaeOnD8qmqvcCfAt8H7gMerqq/GW+rRubUqrqv2/4BcOo4G9PHJIfGqpXkOOCzwO9W1T+Ouz3LJclrgPur6mvjbssITQFnAx+tqrOAR5iAIYsj0Y3jb2YQmM8Bjk3yG+Nt1eh1r1ht/hmISQ6NVblcSZKjGATGtVX1uXG3Z5mdB1yU5O8ZDD++Isl/HW+Tlt0eYE9VHexB3sAgRFayVwLfrap9VfU48Dngl8fcplH5YZJnA3R/3j/m9sxrkkNj1S1XkiQMxrrvqqoPjbs9y62q3llV66vqNAb/f2+qqhX9L9Cq+gFwT5Lnd4fOB3aOsUmj8H3g3CRP637Hz2eFT/4PmQYu6bYvAT4/xrb0MrZVbo/UGJYracF5wG8C30zy9e7YH1bV9vE1ScvgLcC13T+GdgNvGnN7llVV3ZrkBuB2BncI3sGELa3RR5JPAy8HTkmyB3gP8H7g+iRbgO8BbxhfC/txGRFJUm+TPDwlSRoxQ0OS1JuhIUnqzdCQJPVmaEiSejM0JEm9GRqSpN7+H+fClEnhaYFAAAAAAElFTkSuQmCC\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "def make_step_function(x):\n", - " if x < 4:\n", - " return 0.001\n", - " elif x < 6:\n", - " return 0.3\n", - " elif x < 8:\n", - " return 0.5\n", - " elif x < 9:\n", - " return 0.95\n", - " else:\n", - " return 0.9999\n", - "\n", - "\n", - "x = np.arange(0, 10, 0.001)\n", - "probs = [make_step_function(x_) for x_ in x]\n", - "\n", - "y = np.array([1 if np.random.rand() < prob else 0 for prob in probs])\n", - "\n", - "fig, ax = plt.subplots()\n", - "ax2 = ax.twinx()\n", - "\n", - "ax.hist(x[y == 0], alpha=0.15)\n", - "ax.hist(x[y == 1], alpha=0.15)\n", - "ax2.plot(x, probs, color=\"black\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The light blue histogram indicates the distribution of class 0 (`y=0`), while the light orange histogram indicates the distribution of class 1 (`y=1`).
\n", - "The black line indicates the probability function that isused to assign class 0 or 1. In this toy example, it's a step function." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 5/5 [00:00<00:00, 17985.87it/s]\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "counts by TreeBucketer: [4000 1998 2001 936 1065]\n", - "counts by QuantileBucketer: [625 625 625 625 625 625 625 625 625 625 625 625 625 625 625 625]\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "# Try a tree bucketer\n", - "tb = TreeBucketer(\n", - " inf_edges=True,\n", - " max_depth=4,\n", - " criterion=\"entropy\",\n", - " min_samples_leaf=400, # Minimum number of entries in the bins\n", - " min_impurity_decrease=0.001,\n", - ").fit(x, y)\n", - "\n", - "counts_tree, boundaries_tree = tb.counts_, tb.boundaries_\n", - "\n", - "df_tree = pd.DataFrame({\"x\": x, \"y\": y, \"probs\": probs})\n", - "\n", - "df_tree[\"label\"] = pd.cut(x, bins=boundaries_tree, include_lowest=True)\n", - "\n", - "# Try a quantile bucketer\n", - "myQuantileBucketer = QuantileBucketer(bin_count=16)\n", - "myQuantileBucketer.fit(x)\n", - "q_boundaries = myQuantileBucketer.boundaries_\n", - "q_counts = myQuantileBucketer.counts_\n", - "\n", - "df_q = pd.DataFrame({\"x\": x, \"y\": y, \"probs\": probs})\n", - "df_q[\"label\"] = pd.cut(x, bins=q_boundaries, include_lowest=True)\n", - "\n", - "\n", - "fig, ax = plt.subplots(1, 2, figsize=(12, 5))\n", - "\n", - "for label in df_tree.label.unique():\n", - " df_tree[df_tree.label == label].plot(ax=ax[0], x=\"x\", y=\"probs\", legend=False)\n", - " ax[0].scatter(df_tree[df_tree.label == label][\"x\"].mean(), df_tree[df_tree.label == label][\"y\"].mean())\n", - " ax[0].set_title(\"Tree bucketer\")\n", - "\n", - "for label in df_q.label.unique():\n", - " df_q[df_q.label == label].plot(ax=ax[1], x=\"x\", y=\"probs\", legend=False)\n", - " ax[1].scatter(df_q[df_q.label == label][\"x\"].mean(), df_q[df_q.label == label][\"y\"].mean())\n", - " ax[1].set_title(\"Quantile bucketer\")\n", - "\n", - "print(f\"counts by TreeBucketer: {counts_tree}\")\n", - "print(f\"counts by QuantileBucketer: {q_counts}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Comparing the `TreeBucketer` and the `QuantileBucketer` (the dots compare the average distribution of class 1 in the bin):
\n", - "Each buckets obtained by the `TreeBucketer` follow the probability distribution (i.e. the entries in the bucket have the same probability of being class 1).
\n", - "On the contrary, the `QuantileBucketer` splits the values below 4 in 6 buckets, which all have the same probability of being class 1.
\n", - "Note also that the tree is grown with the maximum depth of 4, which potentially lets it grow up to 16 buckets ($2^4$).
\n", - "\n", - "The learned tree is visualized below, whreere the splitting according to the step function is visualized clearly.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "from sklearn.tree import plot_tree\n", - "\n", - "fig, ax = plt.subplots(figsize=(12, 5))\n", - "tre_out = plot_tree(tb.tree, ax=ax)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.3-final" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/tutorials/nb_distribution_statistics.ipynb b/docs/tutorials/nb_distribution_statistics.ipynb deleted file mode 100644 index 9c1e87a2..00000000 --- a/docs/tutorials/nb_distribution_statistics.ipynb +++ /dev/null @@ -1,513 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Univariate Distribution Similarity\n", - "\n", - "[![open in colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ing-bank/probatus/blob/master/docs/tutorials/nb_distribution_statistics.ipynb)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are many situations when you want to perform univariate distribution comparison of a given feature, e.g. stability of the feature over different months.\n", - "\n", - "In order to do that, you can use statistical tests. In this tutorial we present how to easily do this using the `DistributionStatistics` class, and with the statistical tests directly.\n", - "\n", - "Available tests:\n", - "- `'ES'`: Epps-Singleton\n", - "- `'KS'`: Kolmogorov-Smirnov\n", - "- `'PSI'`: Population Stability Index\n", - "- `'SW'`: Shapiro-Wilk\n", - "- `'AD'`: Anderson-Darling\n", - "\n", - "Details on the available tests can be found [here](https://ing-bank.github.io/probatus/api/stat_tests.html#available-tests).\n", - "\n", - "You can perform all these tests using a convenient wrapper class called `DistributionStatistics`.\n", - "\n", - "In this tutorial we will focus on how to perform two useful tests: Population Stability Index (widely applied in banking industry) and Kolmogorov-Smirnov." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install probatus" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "from probatus.binning import QuantileBucketer\n", - "from probatus.stat_tests import DistributionStatistics, ks, psi" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's define some test distributions and visualize them. For these examples, we will use a normal distribution and a shifted version of the same distribution." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "counts = 1000\n", - "np.random.seed(0)\n", - "d1 = pd.Series(np.random.normal(size=counts), name=\"feature_1\")\n", - "d2 = pd.Series(np.random.normal(loc=0.5, size=counts), name=\"feature_1\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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PAS8DuzFCSBn8xc4y04PAD3AIzh7K2czhPNnD3DI91lERBaIGp2LiIiIiIgsE8rgiYiIiIiILBMK8ERERCQoY8y9vgbq03/du9BzExGRmbREU0REREREZJlQBk9ERERERGSZSFroCUSqqKjIVldXL/Q0REREREREFsTevXs7rbXFga4tuQCvurqaPXv2LPQ0REREREREFoQxpj7YNS3RFBERERERWSYU4ImIiIiIiCwTCvBERERERESWiSW3B09ERERERBa/sbExmpqaGB4eXuipLFlpaWlUVlaSnJwc9jMK8EREREREJOaamprIzs6muroaY8xCT2fJsdbS1dVFU1MTNTU1YT+nJZoiIiIiIhJzw8PDFBYWKriLkjGGwsLCiDOgCvBERERERCQuFNzNTTTfPwV4IiIiIiIiMVBXV8cDDzwQ8XN33HEHjzzySEzmoABPREREREQkBqIN8GJJAZ6IiIiIiCxLP/nJTzjvvPPYvn07H/7wh6mvr2ft2rV0dnbi9Xq55JJLeOKJJ6irq2PDhg3cfvvtbN26lZtvvhm32w3A3r17ueyyyzjnnHO4+uqraWlpAeDYsWNceeWVbNu2jbPPPpvjx49z1113sXv3brZv3843vvENPB4Pf/d3f8e5557L1q1b+c53vgM4BVQ+/vGPs2nTJq677jra29tj9jWriqaIiIiIiMTV3/9qPwea+2M65qaKHP6/6zcHvX7w4EEeeughnnnmGZKTk/nYxz7Grl27uPPOO/nIRz7C+eefz6ZNm7jqqquoq6vj8OHD3H///Vx00UW8//3v59vf/jZ//dd/zSc+8QkeffRRiouLeeihh/j85z/P97//fW677TbuuusubrrpJoaHh/F6vXzta1/j3/7t3/j1r38NwH333Udubi4vvfQSIyMjXHTRRVx11VW88sorHD58mDfeeIO2tjY2bdrE+9///ph8XxTgiYiILCVjQ9DwPKw4B9JyFno2IiKL1h/+8Af27t3LueeeC8DQ0BAlJSV8+ctf5uGHH+bee+/l1Vdfnbi/qqqKiy66CID3vOc93H333VxzzTXs27ePt771rQB4PB7Ky8txuVycPHmSm266CXD61QXyxBNP8Prrr0/sr+vr6+Po0aM8/fTT3HrrrSQmJlJRUcFb3vKWmH3dCvBERESWksc/D3vuh9It8OFdkJC40DMSEZlV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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "from probatus.utils.plots import plot_distributions_of_feature\n", - "\n", - "feature_distributions = [d1, d2]\n", - "sample_names = [\"expected\", \"actual\"]\n", - "plot_distributions_of_feature(feature_distributions, sample_names=sample_names, plot_perc_outliers_removed=0.01)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Binning - QuantileBucketer\n", - "\n", - "To visualize the data, we will bin the data using a quantile bucketer, available in the `probatus.binning` module.\n", - "\n", - "Binning is used by all the `stats_tests` in order to group observations." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Bincounts for d1 and d2:\n", - "[100 100 100 100 100 100 100 100 100 100]\n", - "[ 25 62 50 68 76 90 84 169 149 227]\n" - ] - } - ], - "source": [ - "bins = 10\n", - "myBucketer = QuantileBucketer(bins)\n", - "d1_bincounts = myBucketer.fit_compute(d1)\n", - "d2_bincounts = myBucketer.compute(d2)\n", - "\n", - "print(\"Bincounts for d1 and d2:\")\n", - "print(d1_bincounts)\n", - "print(d2_bincounts)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's plot the distribution for which we will calculate the statistics." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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+gwa1LcfyQ033JDx48dn2LBhGTt2bE499dTuO2q99NJL2WWXXbL33nun1tp9zqc//emMGjUqv/nNb5Ikxx57bMaPH5/vfve72XbbbTNs2LDsueeeeeCBBxa71ssvv5zzzz8/b37zm7Puuutmm222yZVXXvmamr7zne9kt912y/DhwzNy5Mh84AMfyJw5czJt2rRccMEFmTNnTkopKaXk2GOP7T7v3//937PXXntlvfXWy8iRI/OJT3wizz333GJj33777dlpp50ybNiw7LrrrvnZz37WV28lAAAArDUEQqvJDTfckJNPPjmTJk3K9ddfnx122CHHH398r8Z65JFHVmh/oeuuuy6HHHJIdtttt9x4442ZOnVqLrnkkpx+emuDrKFDh+aqq67KrFmz8o//+I9JkltvvTUXXXRRLrrooowdO7Z7rDlz5uSzn/1szjzzzFx99dV55plnst9+++XFF1/s7vPpT386X/ziF3PCCSfkpptuyuTJk3P88cfn+9//fnefr3/96znkkEOy9dZb57rrrsvXvva1bLPNNpk/f34+/vGP56Mf/WjGjh2bWbNmZdasWTnzzDOTJHfccUcmTJiQsWPHZubMmbnwwgtz880357jjjuse+4knnsj++++fjTbaKDNnzsyf//mf58gjj8zzzz/fq/cZAAAA1laWjK0m5557biZOnJjp06cnSfbbb7/Mnz+/V8vCSikZPHjwMmcT1Vrz+c9/PkcffXQuvvji7vZ11103J598ck4//fSMHDkyO+ywQ6ZNm5Yzzjgje+65Z4477rgcdthh+chHPrLYeE899VRuuOGGvOtd70qS7Lrrrtl6660zY8aMnHjiiZk9e3amT5+er33taznmmGOSJO973/syb968nH322Zk0aVJefvnlnHbaaZk8eXKuueaa7rEPOuig7uNNNtkk6667bnbffffFrn/aaaflXe96V6699trutnHjxmXChAm577778ra3vS0XXnhhhg0blptuuinrrbdekmT99dfPn/3Zn63sWwwAAABrNTOEVoOFCxfm3nvvzcEHH7xY+yGHHNKr8bbYYossWLAgRx999FL7PPzww3n00Ufz4Q9/OAsWLOj+ee9735sXX3wx9913X3ffU089NTvttFP23HPPvPjii4sFSIuMGTOmOwxaVMOuu+6aO++8M0ny4x//OIMGDcrkyZMXu96ECRPyq1/9KgsXLsxDDz2UJ554YrFZPSvi+eefz6xZs17zu+y5554ZOnRo7rnnniTJnXfemX333bc7DEp6/x4DAADA2swModVg/vz5WbBgQcaMGbNY+6uf96WnnnoqSfKBD3xgia8/9thj3ceDBw/OYYcdlp///Of5yEc+kpEjR76m/5JqHTNmTPc+SE899VQWLlyYDTfccInXmzdvXn77298mac0CWhlPP/10Fi5cmJNOOiknnXTSUn+X3/zmN9lxxx0Xe2348OHZYIMNVup6AAAAsLYTCK0Go0ePzpAhQ9LV1bVY+6uf96WNNtooSXLJJZdkl112ec3rW221Vffxo48+mrPPPju77LJL/vmf/zkf//jHs8MOOyy31q6urvzJn/xJ9/WGDBmSO+64Y4lL2caMGdO9AfSSNtNelte//vUppWTatGlLDLje+MY3JknGjh37mjpfeOGF/P73v1+p6wEAAMDaTiC0GgwePDg777xzbrjhhpx44ond7ddff32/XfOtb31rxo0bl0ceeSSf+MQnltqv1prjjz8+b3rTmzJr1qy8733vyzHHHJNf/OIXGTp0aHe/rq6u/OxnP+teNvboo4/ml7/8Zffyr/e+971ZuHBhnnnmmey7777LrOnKK6/MgQceuMQ+66yzzmIbVSetfYB23333PPTQQznrrLOW+ru84x3vyBVXXJHnn3++e9lYf77HAAAAMFAJhFaTM844I4ccckg++clPZvLkyfnpT3+aH/zgB6/p98ADD+SBBx7oDkXuvvvubLDBBhk9enT22muvJK07fm299da54oorlrqP0KBBg3LBBRfkqKOOyrPPPpv9998/66yzTn7961/nu9/9bmbOnJn11lsvF110Uf7t3/4td911V9Zdd93MmDEjO+20U84999xMmzate7xRo0blqKOOyjnnnJPhw4fnrLPOypgxY7pvC//Wt741J554Yg4//PCceuqpGT9+fF588cXcf//9efjhh3PZZZdl0KBB+fKXv5wjjzwyRx55ZI444oiUUvKTn/wkRxxxRMaPH59tt902Tz75ZGbMmJG3ve1tGTVqVLbccst8+ctfzoQJEzJo0KAceuihed3rXpdHH300N910U84999xss802mTJlSi666KJMmjQpn/3sZ/PEE0/kvPPOy/Dhw/v2wwQAAIABTiC0mkyePDlf/epXc/755+fKK6/M3nvvncsvvzz77bffYv2uu+66nH322d3PF90Cfq+99sptt92WpDWrZ+HChXn55ZeXec2PfOQjGTFiRP7mb/4mV1xxRQYPHpw3velNmTRpUtZZZ53Mnj07f/VXf5WpU6d2772z9dZb50tf+lKmTJmSgw46KG9/+9uTtDaRPuOMM3Laaadlzpw5GT9+fK655poMGzZssVq32WabXHrppTnrrLMyYsSIbL/99vnYxz7W3eejH/1ohg0blnPPPTeHHnpo9+yf0aNHJ0k+/OEP59Zbb82pp56a+fPn55hjjsmMGTOy55575vbbb8/UqVNz1FFHZeHChdliiy0yceLEbLzxxkladx27+eabc8opp+RDH/pQtttuu3zjG994zWbeAAAA0HSl1trpGjJ+/Ph69913L/X1Bx98MNttt91qrIiejj322Nx3331Z1mc00PkzBgAADHi3ntfpCtYO+5ze6Qr6TCnlnlrr+CW95rbzAAAAAA0jEAIAAABoGHsIsVwzZszodAkAAABAHzJDCAAAAKBhBEIAAAAADSMQAgAAAGgYgRAAAABAwwiEAAAAABpGIAQAAADQMAKhDrrvvvtSSsltt92WJHn22WczderU7Lbbbtlwww0zduzYTJ48OQ8//HDHauzq6sq0adPyyCOP9Ns1pk2bllGjRvXb+AAAAMDiBEJrkEcffTSXXnpp9ttvv8ycOTP/8i//knnz5uVP//RP89hjj3Wkpq6urpx99tn9GggBAAAAq9eQThewSm49rzPX3ef0fhl2q622yn//939n+PDh3W3vfve7s/nmm+eKK67I1KlT++W6AAAAQLOYIbQaXXzxxdlss82y/vrr58ADD8y8efMWe3399ddfLAxKko022ihbbLFFurq6Vvp6s2bNykEHHZQ3vvGNWX/99bPzzjvnm9/85mv6zZkzJ0cccURGjRqV9dZbLzvuuGOuvvrqPPLII9lhhx2SJPvss09KKSmlJElmzJiRUkp+//vfLzbWlltumb/8y7/sfn7TTTdl3333zZgxYzJixIjsvvvuueWWW1b6dwEAAAD6znIDoVLKZqWUW0spD5ZS7i+lfKbdvlEp5UellP9qP76h3V5KKV8ppcwupfxHKeXt/f1LDAQ33HBDTj755EyaNCnXX399dthhhxx//PHLPW/+/PmZPXt2tt9++8XaSymZNm3aMs+dM2dO9thjj1x22WX53ve+lw996EM57rjjcs0113T36erqyjvf+c7cdddd+bu/+7t873vfy8c+9rE89thj2WSTTboDpIsuuiizZs3KrFmzVur3/p//+Z8ceOCB+frXv55vf/vbede73pX9998/d9xxx0qNAwAAAPSdFVkytiDJ52qtvyylvC7JPaWUHyU5NsmPa63nl1JOS3Jakr9Ksn+St7R//jTJ9PZjo5177rmZOHFipk+fniTZb7/9Mn/+/Fx22WXLPO9zn/tcNthggxx++OGLtQ8ePDiDBi07z+t5Tq0173nPezJ37txceumlOeKII5Ik//AP/5Bnnnkm99xzTzbZZJMkyYQJE7rP23HHHZMk22+/fXbfffcV/G1f8alPfar7+OWXX84+++yT+++/P5dffnn22GOPlR4PAAAAWHXLnSFUa51Xa/1l+/i5JA8mGZfk4CRXtrtdmeSD7eODk1xVW36e5PWllE36vPIBZOHChbn33ntz8MEHL9Z+yCGHLPO86dOn5xvf+EYuu+yyjBw5crHXFixYkLPOOmuZ5z/99NM55ZRTssUWW2To0KEZOnRoLrnkksXuWvaTn/wkEydO7A6D+trcuXNzzDHHZNy4cRkyZEiGDh2aW265paN3TgMAAICmW6lNpUspWybZJckvkmxca52XtEKjUsqYdrdxSXreEmtuu23xDXMaZP78+VmwYEHGjBmzWPurn/d044035tOf/nS+9KUvZfLkyb267rHHHpuf//znOfPMM7P99ttnxIgRmT59em644YbuPr/97W/zjne8o1fjL8/LL7+cgw46KM8991y+8IUv5M1vfnPWX3/9nHXWWb3aEwkAAADoGyscCJVSNkjy7SRTaq3PLtpceEldl9BWlzDeCUlOSJLNN998RcsYkEaPHp0hQ4a8JgRZWijys5/9LIcffnhOPPHEfP7zn+/VNV988cXcdNNN+ad/+qeceOKJ3e0vv/zyYv1Gjhz5ms2tV8SwYcOSJH/84x8Xa3/66ae7j2fPnp177703//qv/5qJEyd2t7/wwgsrfT0AAACg76zQXcZKKUPTCoO+WWu9vt385KKlYO3HRenG3CSb9Th90yRPvHrMWusltdbxtdbxo0eP7m39A8LgwYOz8847LzYzJ0muv/761/S9//77M2nSpEycODFf+cpXen3NP/zhD1m4cGHWXXfd7rbnnnsuN95442L9JkyYkB/+8Id58sknlzjOOuusk6QVMPW06aabJkkefPDB7rZf/OIXefbZZ7ufLwp+etYwZ84cG0oDAABAhy13hlBpTQW6PMmDtda/7/HSjUmOSXJ++/GGHu2fKqV8K63NpJ9ZtLSsyc4444wccsgh+eQnP5nJkyfnpz/9aX7wgx8s1qerqysTJ07MBhtskFNOOSV33nln92sjRoxY7E5jQ4YMyVlnnbXUfYQ23HDDvOMd78gXvvCFjBgxIoMGDcr555+fDTfccLHQ5i/+4i9y1VVX5d3vfnf++q//OptttlkefPDB/O///m9OPfXUbL755hk+fHiuvPLKbLjhhhk6dGjGjx+f3XbbLePGjcspp5ySc845J7/73e/y5S9/OSNGjOgee9ttt82mm26az33ucznnnHPy3HPPZerUqRk3blxfva0AAABAL6zIDKE9khyV5L2llF+1fz6QVhC0bynlv5Ls236eJDcn+XWS2UkuTXJS35c98EyePDlf/epX873vfS8f/OAHc++99+byyy9frM8DDzyQuXPn5rHHHss+++yTd77znd0/J520+Nu4cOHC1yz/erWrr746W221VY4++uh85jOfyYc+9KEcffTRi/UZPXp07rjjjuyyyy6ZMmVKJk2alEsuuaR7Gd+wYcNy6aWX5p577slee+3Vvd/QOuusk+985zsZNGhQDj300FxwwQWZPn163vCGN3SPve666+b666/PkCFDcuihh+bMM8/M6aefnr322qvX7yMAAACw6kqtr9neZ7UbP358vfvuu5f6+oMPPpjttttuNVZE0/gzBgAADHi3ntfpCtYO+5ze6Qr6TCnlnlrr+CW9tkJ7CAEAAACw9hAIAQAAADSMQAgAAACgYQRCAAAAAA0jEAIAAABomAETCK0Jd0Nj7eTPFgAAAE0zIAKhoUOH5oUXXuh0GaylXnjhhQwdOrTTZQAAAMBqMyACoTFjxuTxxx/P888/bzYHfabWmueffz6PP/54xowZ0+lyAAAAYLUZ0ukCVsSIESOSJE888UReeumlDlfD2mTo0KHZeOONu/+MAQAAQBMMiEAoaYVC/qcdAAAAYNUNiCVjAAAAAPQdgRAAAABAwwiEAAAAABpGIAQAAADQMAIhAAAAgIYRCAEAAAA0jEAIAAAAoGEEQgAAAAANIxAC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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.figure(figsize=(20, 5))\n", - "plt.bar(range(0, len(d1_bincounts)), d1_bincounts, label=\"d1: expected\")\n", - "plt.bar(range(0, len(d2_bincounts)), d2_bincounts, label=\"d2: actual\", alpha=0.5)\n", - "plt.title(\"PSI (bucketed)\", fontsize=16, fontweight=\"bold\")\n", - "plt.legend(fontsize=15)\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "By visualizing the bins, we can already notice that the distributions are different.\n", - "\n", - "Let's use the statistical test to prove that." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## PSI - Population Stability Index\n", - "The population stability index ([Karakoulas, 2004](https://cms.rmau.org/uploadedFiles/Credit_Risk/Library/RMA_Journal/Other_Topics_(1998_to_present)/Empirical%20Validation%20of%20Retail%20Credit-Scoring%20Models.pdf)) has long been used to evaluate distribution similarity in the banking industry, while developing credit decision models.\n", - "\n", - "In `probatus` we have implemented the PSI according to [Yurdakul 2018](https://scholarworks.wmich.edu/cgi/viewcontent.cgi?article=4249&context=dissertations), which derives a p-value, based on the hard to interpret PSI statistic. Using the p-value is a more reliable choice, because the banking industry-standard PSI critical values of 0.1 and 0.25 are unreliable heuristics as there is a strong dependency on sample sizes and number of bins. Aside from these heuristics, the PSI value is not easily interpretable in the context of common statistical frameworks (like a p-value or confidence levels)." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "PSI = 0.33942407655561885\n", - "\n", - "PSI: Critical values defined according to de facto industry standard:\n", - "PSI > 0.25: Significant distribution change; investigate.\n", - "\n", - "PSI: Critical values defined according to Yurdakul (2018):\n", - "99.9% confident distributions have changed.\n" - ] - } - ], - "source": [ - "psi_value, p_value = psi(d1_bincounts, d2_bincounts, verbose=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Based on the above test, the distribution between the two samples significantly differ.\n", - "Not only is the PSI statistic above the commonly used critical value, but also the p-value shows a very high confidence." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## PSI with DistributionStatistics " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Using the `DistributionStatistics` class one can apply the above test without the need to manually perform the binning. We initialize a `DistributionStatistics` instance with the desired test, binning_strategy (or choose `\"default\"` to choose the test's most appropriate binning strategy) and the number of bins. Then we start the test with the unbinned values as input." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "PSI = 0.33942407655561885\n", - "\n", - "PSI: Critical values defined according to de facto industry standard:\n", - "PSI > 0.25: Significant distribution change; investigate.\n", - "\n", - "PSI: Critical values defined according to Yurdakul (2018):\n", - "99.9% confident distributions have changed.\n" - ] - } - ], - "source": [ - "distribution_test = DistributionStatistics(\"psi\", binning_strategy=\"default\", bin_count=10)\n", - "psi_value, p_value = distribution_test.compute(d1, d2, verbose=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## KS: Kolmogorov-Smirnov with DistributionStatistics\n", - "The Kolmogorov-Smirnov test compares two distributions by calculating the maximum difference of the two samples' distribution functions, as illustrated by the black arrow in the following figure. The KS test is available in `probatus.stat_tests.ks`.\n", - "\n", - "\"Example\n", - "\n", - "The main advantage of this method is its sensitivity to differences in both location and shape of the empirical cumulative distribution functions of the two samples.\n", - "\n", - "The main disadvantages are that: it works for continuous distributions (unless modified, e.g. see ([Jeng 2006](https://bmcmedresmethodol.biomedcentral.com/track/pdf/10.1186/1471-2288-6-45))); in large samples, small and unimportant differences can be statistically significant ([Taplin & Hunt 2019](https://www.mdpi.com/2227-9091/7/2/53/pdf)); and finally in small samples, large and important differences can be statistically insignificant ([Taplin & Hunt 2019](https://www.mdpi.com/2227-9091/7/2/53/pdf))." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As before, using the test requires you to perform the binning beforehand" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "KS: pvalue = 2.104700973377179e-27\n", - "\n", - "KS: Null hypothesis rejected with 99% confidence. Distributions very different.\n" - ] - } - ], - "source": [ - "k_value, p_value = ks(d1, d2, verbose=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Again, we can also choose to combine the binning and the statistical test using the `DistributionStatistics` class." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "KS: pvalue = 2.104700973377179e-27\n", - "\n", - "KS: Null hypothesis rejected with 99% confidence. Distributions very different.\n" - ] - } - ], - "source": [ - "distribution_test = DistributionStatistics(\"ks\", binning_strategy=None)\n", - "ks_value, p_value = distribution_test.compute(d1, d2, verbose=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## AutoDist" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "from probatus.stat_tests import AutoDist" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Multiple statistics can automatically be calculated using `AutoDist`. To show this, let's create two new dataframes with two features each." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "size, n_features = 100, 2\n", - "\n", - "df1 = pd.DataFrame(np.random.normal(size=(size, n_features)), columns=[f\"feat_{x}\" for x in range(n_features)])\n", - "df2 = pd.DataFrame(np.random.normal(size=(size, n_features)), columns=[f\"feat_{x}\" for x in range(n_features)])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now specify the statistical tests we want to perform and the binning strategies to perform. We can also set both of these variables to `'all'` or binning strategies to `'default'` to use the default binning strategy for every chosen statistical test." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "statistical_tests = [\"KS\", \"PSI\"]\n", - "binning_strategies = \"default\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's compute the statistics and their p_values:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 2/2 [00:00<00:00, 141.92it/s]\n", - "100%|██████████| 2/2 [00:00<00:00, 139.13it/s]\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
columnp_value_KS_no_bucketing_0p_value_PSI_quantilebucketer_10statistic_KS_no_bucketing_0statistic_PSI_quantilebucketer_10
0feat_00.8154150.4432440.090.192113
1feat_10.2819420.0109220.140.374575
\n", - "
" - ], - "text/plain": [ - " column p_value_KS_no_bucketing_0 p_value_PSI_quantilebucketer_10 \\\n", - "0 feat_0 0.815415 0.443244 \n", - "1 feat_1 0.281942 0.010922 \n", - "\n", - " statistic_KS_no_bucketing_0 statistic_PSI_quantilebucketer_10 \n", - "0 0.09 0.192113 \n", - "1 0.14 0.374575 " - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "myAutoDist = AutoDist(statistical_tests=statistical_tests, binning_strategies=binning_strategies, bin_count=10)\n", - "myAutoDist.compute(df1, df2)" - ] - } - ], - "metadata": { - "environment": { - "name": "common-cpu.m48", - "type": "gcloud", - "uri": "gcr.io/deeplearning-platform-release/base-cpu:m48" - }, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/tutorials/nb_imputation_comparison.ipynb b/docs/tutorials/nb_imputation_comparison.ipynb deleted file mode 100644 index c6cf2cd2..00000000 --- a/docs/tutorials/nb_imputation_comparison.ipynb +++ /dev/null @@ -1,324 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Imputation Comparison\n", - "\n", - "[![open in colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ing-bank/probatus/blob/master/docs/tutorials/nb_imputation_comparison.ipynb)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This notebook explains how the `ImputationSelector` class works in `probatus`. With `ImputationSelector` you can compare multiple imputation strategies\n", - "and choose a strategy which works the best for a given model and a dataset.\n", - "Currently `ImputationSelector` supports any [scikit-learn](https://scikit-learn.org/stable/) compatible imputation strategy. For categorical variables the missing values are replaced by a `missing` token and `OneHotEncoder` is applied. The user-supplied imputation strategies are applied to numerical columns only. \n", - "Support for user-supplied imputation strategies for categorical columns can be added in the future releases.\n", - "\n", - "Let us look at an example and start by importing all the required classes and methods.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "###Install the packages\n", - "# %%capture\n", - "#!pip install probatus\n", - "#!pip install lightgbm" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)\n" - ] - } - ], - "source": [ - "%matplotlib inline\n", - "%load_ext autoreload\n", - "%autoreload 2\n", - "import pandas as pd\n", - "\n", - "pd.set_option(\"display.max_columns\", 100)\n", - "pd.set_option(\"display.max_row\", 500)\n", - "pd.set_option(\"display.max_colwidth\", 200)\n", - "import lightgbm as lgb\n", - "from sklearn.datasets import make_classification\n", - "from sklearn.experimental import enable_iterative_imputer\n", - "\n", - "from sklearn.impute import IterativeImputer, KNNImputer, SimpleImputer\n", - "from sklearn.linear_model import LogisticRegression\n", - "\n", - "from probatus.missing_values.imputation import ImputationSelector\n", - "from probatus.utils.missing_helpers import generate_MCAR" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's create a classification dataset to apply the various imputation strategies.\n", - "\n", - "We'll use the `probatus.utils.missing_helpers.generate_MCAR` function to randomly add missing values to the dataset." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Shape of X,y : (2000, 20),(2000,)\n" - ] - } - ], - "source": [ - "n_features = 20\n", - "X, y = make_classification(n_samples=2000, n_features=n_features, random_state=123, class_sep=0.3)\n", - "X = pd.DataFrame(X, columns=[\"f_\" + str(i) for i in range(0, n_features)])\n", - "print(f\"Shape of X,y : {X.shape},{y.shape}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
0
f_00.2080
f_10.1960
f_20.1990
f_30.2095
f_40.2150
\n", - "
" - ], - "text/plain": [ - " 0\n", - "f_0 0.2080\n", - "f_1 0.1960\n", - "f_2 0.1990\n", - "f_3 0.2095\n", - "f_4 0.2150" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "X_missing = generate_MCAR(X, missing=0.2)\n", - "missing_stats = pd.DataFrame(X_missing.isnull().mean())\n", - "\n", - "missing_stats.head()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The data has approximately 20% missing values in each feature." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Imputation Strategies" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a dictionary with all the strategies to compare. Also, create a classifier to use for evaluating various strategies.\n", - "If the model supports handling of missing features by default then the model performance on an unimputed dataset is calculated. You can indicate that the model supports handling missing values by setting the parameter `model_na_support=True`.\n", - "The model performance against the unimputed dataset can be found in `No Imputation` results." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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sHtw7ddHSQh37u7u7Nmzx2Zfam2xExEREUmQqkfFWiwW69qv69ev599//6VMmTJkypQJNze3JI8pUqQIn3/+Oblz507OqoqIiIi8cKk62CWEug8//JCvv/4ad3d34uPj6dOnD40aNSIoKCjJ4/LmzQvYBkMRERGR1C5Vppr7x3ts3ryZLVu28MMPP3D48GFatWrFzJkzmT59OufPn3/keRTqRERExJGkyha7hOlIZs2axa5duyhevDjlypUD7q0g4ebmxvz58zGZTLRr1+6hLXciIiIijiRVBrsEa9as4fvvv6dcuXJER0djNpsBGDJkCAALFy4kKiqKfv36ERgYaM+qioiIiLxwqeZZZFLT7c2fP59u3boRGRnJN998w/Xr1637hgwZQuXKlTl37hwBAQHJWVURERERu0gVLXb3D3K4c+cOcXFx1ta58ePHExUVxcSJE3F1deX999+3Tt43fvx4rf0qIiIiaUaKb7G7P9SNGjWKBg0aUKhQIcaOHcuuXbsA+OqrryhXrhzjxo1jwYIFNi13CnUiIiKSVqTYJcUenIpkwIABfPnllwwYMACTycSECRN47bXX+OCDDwgNDQWgTZs2LFy4kPDwcGrXrm2vqicr6zIjfX3wdVd4FRGxu8HX/7uMyBNwiCXF7g91P/zwAwsXLmT58uV07dqVUqVKceLECXbs2MGECRPYsmULAF9++SV9+vShZs2a9qq2iIjIf7p48SI1a9bE29ub/Pnzs379+iTLFSxYELPZbH05OzvTpUuXROXat2+PyWTin3/+sdk+evRosmbNio+PD0WLFuXGjRsv5Hok5UhxfewaNGjASy+9xOTJk4F7gybSp0/PBx98QKlSpVixYgVNmzZl1qxZvPTSS9SpUwdXV1du3LhBWFgY/fv3ByA+Pl7LhImISIrUqVMngoKCuHjxIj/99BMNGjQgMjIy0WC/AwcOWP8dExNDUFAQ9erVsymza9cuDh48mOg9pkyZwurVq/ntt9/ImjUr+/bte+iqTOI4UtSj2JiYGLZs2ULZsmVtfvmuXLlCfHw8rq6u1KlThxo1atCnTx8AChUqxMWLF+nQoQODBw+2U83tR49iRURSmP94FBsdHU1AQADHjh0jODgYgAoVKtC8eXNatmz50OMiIiLo1asXJ06csPYbNwyD8uXLM3HiREqUKMHp06cJDg4mPj6e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GKYhUf9d6BEU7OR5eZJHscmSlv755x88PT2tS5y89NJL9OvXD4vFQr169fj++++pX78+c+bMYdq0aWzatImlS5eSLVs21q5di4uLC/Hx8RpAISIiySa6n8+jCww4lzwVEXkCL7zFbtGiRbRp04bMmTPTtm1bXnrpJetw7tjYWD788EMmT57MggULePfdd60DKm7duoWXlxdwb14etdglTS12IiJ2osETkkxSTItdbGwsGzZsIC4ujkuXLrFkyRJOnDjB8OHDyZcvHx07duTtt9/Gx8eH999/Hz8/P6pWrQpgDXWGYSjUiYiIiDyGF5qY3Nzc+OSTT3B1deX48eMULFiQiIgIlixZwurVq2nXrh137twhT548ODk5Ua1aNX7//XeKFy9uPYeWCRMRERF5PC+8Keyll17io48+Yvjw4axbt44sWbLQqVMn2rVrx19//cX58+eZPXs2MTExXL58mVdfffVFV0lERETEISXbqNhz584xfPhwduzYQZ06dejfv791n2EYmEwm6/+qT93jUx87ERE7UR87SSYpcoLiTJkyMWDAAEqVKsWyZcsYNWqUdV/CpMQmkwmLxaJQJyIiIvIUknXliaCgIJtwN3DgQACbIJewGoWjenCdXBEREZHnJdlTVFBQEP379yd37tz8+++/aSrgGIZhDa47duywc21ERETE0dileSwoKIgJEyYwbdo0a986R2exWKwjfD/66CNKly7NmTNn7FwrERERcSR268wWEBAAYJ2Q2NElXOOhQ4e4desWP//8M1myZLFzrURERMSR2D1RpYVQlyAiIoKwsDA2bdpEzpw5n6qlMiYmhqioKJuXiIiICNixxS4tcnZ2pmDBgvz8889cvnyZ4ODgJ26xHDFiBEOGDEm0vdCdr3AyvJ5ndUVE5FH6rrR3DezqxMia9q6CJCHtNJcls4TRr/d755136NOnD6+++ipNmzblwIEDODk5JVn2Yfr168f169etr9OnTz/PaouIiDx3Fy9epGbNmnh7e5M/f37Wr1+fZLkTJ04QFhaGv78/WbJk4bPPPrPZv337dl5//XXMZjNZs2Zl0aJF1n03btygTZs2BAQE4O/vT7NmzV7oNaVUarF7Ae5vhVu8eDEXL17k+vXrNG7cmAoVKjBq1CiGDx9OmzZt+Oqrr3jllVceu+XO3d0dd3f3F30JIiIiz02nTp0I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MmbMyPTp07lz5w4///wzGTNmtDmHs7OzHa8i+WjwhIiIiGNLtYMnwsPDmTBhAgULFuSTTz4he/bsxMbGUrZsWU6cOMHSpUspXLgwf/zxB127duXatWscPnyYHj162JwnrYQ6ERERkfuliBY7wzCIj4+nZMmStG3blo4dOwKwdetWxo4dy7///suHH35I7dq1rcds27aNv//+m99++42JEyfi6upqr+rblVrsREREHFuqa7EzmUzExcVx7do1PD09rdtLly7Nhx9+SMaMGfnf//7HokWLrPtef/11GjduzNSpU3F1deXu3bv2qLqIiIhIipEigl0CHx8fXnrpJQAsFgtwL8B9+OGHpE+fns8//5wffvghyWPTaoudiIiISAK7Brv169fz2WefAfeC2c2bN/Hx8QHuPZ5NeEr8+uuv07t3bzJkyED//v355Zdf7FZnERERkZTKbtOdxMTE8P3337Nt2za8vb1p3bo1MTEx1keqDw6AeP311+nZsyfLli2jbNmy9qiyiIiISIpm18ETZ8+eZfTo0Wzfvp0yZcqwfPlyGjdujI+PDxaLBXd3dwBu377NhQsXaNOmDS+//DKQtqY0eRQNnhAREXFsT/Jdb/dRsefOnWPYsGFs2rSJgwcPkjt3bnx8fLh58yYWiwWTyYSHhwf+/v5s2LBBy4M9QMFORETEsT3Jd73dU1KmTJkYOHAgzs7O+Pv7U6FCBWu/u9jYWFxcXHBycsIwDEwmk1rqRERERB4iRYyKDQoKom/fvhQtWpS1a9cyYsQIANzc3IiPjwfuTYliGIZCnYiIiMhDpIhgB/da7gYMGMDrr7/OihUrGDhwIGA7jYnJZLJX9URERERSvBQT7OBey13//v3JnTs3//77LylgUQwRERGRVMPufeweFBQUxIQJE/D397c+flVLnYiIiMh/S3HBDiAgIAC4t/qEk1OKalQUERERSbFSdGpSqBMRERF5fEpOIiIiIg5CwU5ERETEQSjYiYiIiDgIBTsRERERB6FgJyIiIuIgFOxEREREHISCnYiIiIiDULATERERcRAKdiIiIiIOQsFORERExEEo2ImIiIg4CAU7EREREQehYCciIiLiIFzsXQF5NoZhABAVFWXnmoiIiMiLkPAdn/Cd/ygKdqnc5cuXAciaNaudayIiIiIv0o0bN/Dz83tkGQW7VC4gIACAU6dO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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "strategies = {\n", - " \"KNN Imputer\": KNNImputer(n_neighbors=3),\n", - " \"Median Imputer\": SimpleImputer(strategy=\"median\", add_indicator=True),\n", - " \"Iterative Imputer\": IterativeImputer(add_indicator=True, n_nearest_features=5, sample_posterior=True),\n", - "}\n", - "\n", - "clf = lgb.LGBMClassifier(n_estimators=2)\n", - "cmp = ImputationSelector(clf=clf, strategies=strategies, cv=5, random_state=45, model_na_support=True)\n", - "cmp.fit_compute(X_missing, y)\n", - "result_plot = cmp.plot()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "However if the model does not support missing values by default (e.g. `LogisticRegression`), results for only the imputation strategies are calculated. \n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAABzt0lEQVR4nO3dd1hT1+M/8HcSNiFQQAoK4t4o7oloxSou+nHWiXsv3Kgtbpx1r7rFLXWiVq2rWlcrTly4B6AWFQwyk/P7wx/3awQtqBAI79fz5Ck5Offm3FxS3p5zz7kyIYQAEREREeV6cn03gIiIiIi+DgY7IiIiIgPBYEdERERkIBjsiIiIiAwEgx0RERGRgWCwIyIiIjIQDHZEREREBoLBjoiIiMhAMNgRERERGQgGOyLKdg8ePIBMJsPatWv13ZQsJZPJMHDgwK+6z0KFCqFr165fdZ+kH127dkWhQoX03QwyMAx2RNls7dq1kMlk+Oeff/TdlE+6fv06JkyYgAcPHnz2PjZt2oR58+Z9tTZ9iePHj0Mmk2XoQdkjISEBc+fORfXq1WFtbQ0zMzOUKFECAwcOxO3bt/XdPKJcyUjfDSCinOn69euYOHEi6tWr99m9Cps2bcK1a9cwdOhQnXJXV1fEx8fD2Nj4yxuaQaVLl0ZQUJBOmb+/P5RKJcaNG5dt7fhSt27dglye+/9N/u+//6Jx48a4cOECmjVrhg4dOkCpVOLWrVvYsmULfv31VyQlJem7mVlqxYoV0Gq1+m4GGRgGOyLKdjKZDGZmZtn6nt9++y06deqkUzZ9+nTY29unKc/JTE1N9d2Er6Jr1664ePEigoOD0apVK53XJk+enKvCdmbFxcXB0tIyW/9hQ3lH7v9nH5EB6Nq1K5RKJR49eoRmzZpBqVSiQIECWLx4MQDg6tWr+O6772BpaQlXV1ds2rRJZ/vU4d0///wTffr0gZ2dHVQqFbp06YJXr17p1JXJZJgwYUKaNrx/7dbatWvRpk0bAED9+vWlIcrjx48DAHbv3o2mTZsif/78MDU1RdGiRTF58mRoNBppf/Xq1cO+ffvw8OFDafvUnr+PXWN39OhReHh4wNLSEjY2NvDx8cGNGzd06kyYMAEymQx37txB165dYWNjA2tra3Tr1g1v377NzMeertevX2Po0KFwcXGBqakpihUrhhkzZqTpWdFqtZg/fz7c3NxgZmaGfPnyoXHjxukOse/atQvlypWDqakpypYti99///2zjym9a+zCwsLw3XffwdzcHM7OzpgyZQpWr14NmUymM5SekXOf2c9hy5YtqFy5MqysrKBSqeDm5ob58+d/5NN959y5c9i3bx969OiRJtQB78Lr7Nmzdcoy87tx+/ZtdOrUCdbW1siXLx9++uknCCHw+PFj+Pj4QKVSwdHREXPmzNHZPnW4fuvWrRg7diwcHR1haWmJFi1a4PHjxzp1T548iTZt2qBgwYIwNTWFi4sL/Pz8EB8fr1Mv9bt99+5dNGnSBFZWVujYsaP02oe94Rn5PO/du4c2bdrA1tYWFhYWqFGjBvbt25fusWzbtg1Tp06Fs7MzzMzM0KBBA9y5c+cjZ4YMAXvsiHIIjUYDb29v1K1bFzNnzsTGjRsxcOBAWFpaYty4cejYsSNatmyJZcuWoUuXLqhZsyYKFy6ss4+BAwfCxsYGEyZMwK1bt7B06VI8fPhQ+p98RtWtWxeDBw/GggULMHbsWJQuXRoApP+uXbsWSqUSw4YNg1KpxNGjR/Hzzz8jNjYWs2bNAgCMGzcOMTExePLkCebOnQsAUCqVH33PP/74A97e3ihSpAgmTJiA+Ph4LFy4ELVr10ZoaGiaP4Bt27ZF4cKFERgYiNDQUKxcuRIODg6YMWNGho/zQ2/fvoWnpyeePn2KPn36oGDBgjh9+jT8/f0RGRmpc71gjx49sHbtWnh7e6Nnz55ISUnByZMncfbsWVSpUkWqd+rUKezYsQP9+/eHlZUVFixYgFatWuHRo0ews7P74mOKiopC/fr1kZKSgjFjxsDS0hK//vorzM3Ns/xzOHz4MNq3b48GDRpIbbxx4wb++usvDBky5KP737NnDwCgc+fOGWpPZn832rVrh9KlS2P69OnYt28fpkyZAltbWyxfvhzfffcdZsyYgY0bN2LEiBGoWrUq6tatq7P91KlTIZPJMHr0aDx//hzz5s2Dl5cXLl26JH2u27dvx9u3b9GvXz/Y2dnh/PnzWLhwIZ48eYLt27fr7C8lJQWNGjVCnTp1MHv2bFhYWKR7nBn5PJ89e4ZatWrh7du3GDx4MOzs7LBu3Tq0aNECwcHB+N///qezz+nTp0Mul2PEiBGIiYnBzJkz0bFjR5w7dy5Dnz3lQoKIstWaNWsEAPH3339LZb6+vgKAmDZtmlT26tUrYW5uLmQymdiyZYtUfvPmTQFABAQEpNln5cqVRVJSklQ+c+ZMAUDs3r1bKvtw21Surq7C19dXer59+3YBQBw7dixN3bdv36Yp69Onj7CwsBAJCQlSWdOmTYWrq2uauvfv3xcAxJo1a6Qyd3d34eDgIKKjo6Wyy5cvC7lcLrp06SKVBQQECACie/fuOvv83//+J+zs7NK816eULVtWeHp6Ss8nT54sLC0txe3bt3XqjRkzRigUCvHo0SMhhBBHjx4VAMTgwYPT7FOr1Uo/AxAmJibizp07OscEQCxcuPCzjunD8zR06FABQJw7d04qe/78ubC2thYAxP3793Xak5Fzn9HPYciQIUKlUomUlJQ0+/yU//3vfwKAePXqVYbqZ/Z3o3fv3lJZSkqKcHZ2FjKZTEyfPl0qT/1+vX/cx44dEwBEgQIFRGxsrFS+bds2AUDMnz9fKkvvOxAYGChkMpl4+PChVJb63R4zZkya+r6+vjrfj4x8nqnn++TJk1LZmzdvROHChUWhQoWERqPROZbSpUuLxMREqe78+fMFAHH16tWPvgflbhyKJcpBevbsKf1sY2ODkiVLwtLSEm3btpXKS5YsCRsbG9y7dy/N9r1799a5bqdfv34wMjLC/v37v2o73+8NevPmDf799194eHjg7du3uHnzZqb3FxkZiUuXLqFr166wtbWVysuXL4+GDRum2/6+ffvqPPfw8EB0dDRiY2Mz/f6ptm/fDg8PD3zzzTf4999/pYeXlxc0Gg3+/PNPAMBvv/0GmUyGgICANPv4sGfUy8sLRYsW1TkmlUqV7vn7nGPav38/atSogWrVqkll+fLlk4b7PkdGPwcbGxvExcXh8OHDmdp/6vFYWVn9Z93P+d14/3ukUChQpUoVCCHQo0cPqTz1+5XeeejSpYtO21q3bg0nJyed93r/OxAXF4d///0XtWrVghACFy9eTLPPfv36/eexZuTz3L9/P6pVq4Y6depIZUqlEr1798aDBw9w/fp1nfrdunWDiYmJ9NzDwwMA0j1uMgwMdkQ5ROp1Wu+ztraGs7NzmrBgbW2d5to5AChevLjOc6VSCScnpy9asiQ9YWFh+N///gdra2uoVCrky5dPmoAQExOT6f09fPgQwLvQ+qHSpUvj33//RVxcnE55wYIFdZ5/8803AJDu55JR4eHh+P3335EvXz6dh5eXFwDg+fPnAIC7d+8if/78OkHjYz5sZ2pb02vn5xzTw4cP05x3IP3PMqMy+jn0798fJUqUgLe3N5ydndG9e/c01w+mR6VSAXj3j4L/8jV+N1KXUrG3t09TnpHvkUwmQ7FixXS+R48ePZLCplKpRL58+eDp6Qkg7XfAyMgIzs7O/3GkGfs8Hz58+NHPIvX192XF94RyNl5jR5RDKBSKTJULIb7q+78/8eFTXr9+DU9PT6hUKkyaNAlFixaFmZkZQkNDMXr06GxbviErPhetVouGDRti1KhR6b5eokSJTO8zM+3MrnP9oQ/PfUY/BwcHB1y6dAkHDx7EgQMHcODAAaxZswZdunTBunXrPvp+pUqVAvBuUlBqD9LXlN7n+DU/W41Gg4YNG+Lly5cYPXo0SpUqBUtLSzx9+hRdu3ZN8x0wNTXN0BI1n/t5foq+fqdIfxjsiAxIeHg46tevLz1Xq9WIjIxEkyZNpLJvvvkGr1+/1tkuKSkJkZGROmUfm2xx/PhxREdHY8eOHToXnd+/fz9N3YxO2HB1dQXwbo22D928eRP29vawtLTM0L6+RNGiRaFWq6WeqU/VO3jwIF6+fJmhXrus5OrqivDw8DTl6X2WGT33Gf0cAMDExATNmzdH8+bNodVq0b9/fyxfvhw//fQTihUrlu42zZs3R2BgIDZs2PCfwU4fvxsffp5CCNy5cwfly5cH8C6Q3r59G+vWrUOXLl2kepkdkk7Pf32erq6uH/0sgP/7vCjv4lAskQH59ddfkZycLD1funQpUlJS4O3tLZUVLVpUukbq/e0+7LVJ/WP5YRBI7QF4/1/8SUlJWLJkSZr2WFpaZmho1snJCe7u7li3bp3O+127dg2HDh3SCaZZqW3btjhz5gwOHjyY5rXXr18jJSUFANCqVSsIITBx4sQ09bK7J6RJkyY4e/Yszp8/L5W9ePECGzduTFM3o+c+o59DdHS0zmtyuVwKP4mJiR9tc82aNdG4cWOsXLkSu3btSvN6UlISRowYAUA/vxvr16/XGSYODg5GZGSk9D1K7zsghPjPZV7+S0Y+zyZNmuD8+fM4c+aMVC8uLg6//vorChUqhDJlynxRGyj3Y48dkQFJSkpCgwYN0LZtW9y6dQtLlixBnTp10KJFC6lOz5490bdvX7Rq1QoNGzbE5cuXcfDgwTTXH7m7u0OhUGDGjBmIiYmBqakpvvvuO9SqVQvffPMNfH19MXjwYMhkMgQFBaUbaCpXroytW7di2LBhqFq1KpRKJZo3b55u22fNmgVvb2/UrFkTPXr0kJa0sLa2TnfttawwcuRI7NmzB82aNUPXrl1RuXJlxMXF4erVqwgODsaDBw9gb2+P+vXro3PnzliwYAHCw8PRuHFjaLVanDx5EvXr1//q94f9lFGjRiEoKAiNGzfGkCFDpOVOXF1dceXKFZ26GT33Gf0cevbsiZcvX+K7776Ds7MzHj58iIULF8Ld3V265utj1q9fj++//x4tW7ZE8+bN0aBBA1haWiI8PBxbtmxBZGSktJZddv9u2Nraok6dOujWrRuePXuGefPmoVixYujVqxeAd0PJRYsWxYgRI/D06VOoVCr89ttvX3zdWkY+zzFjxmDz5s3w9vbG4MGDYWtri3Xr1uH+/fv47bffDOKuJPSF9DATlyhP+9hyJ5aWlmnqenp6irJly6Ypd3V1FU2bNk2zzxMnTojevXuLb775RiiVStGxY0edJSKEEEKj0YjRo0cLe3t7YWFhIRo1aiTu3LmTZskLIYRYsWKFKFKkiFAoFDpLn/z111+iRo0awtzcXOTPn1+MGjVKHDx4MM3yKGq1WnTo0EHY2NgIANLSDuktdyKEEH/88YeoXbu2MDc3FyqVSjRv3lxcv35dp07qkhYvXrxI93N9f3mP//LhcidCvFs6wt/fXxQrVkyYmJgIe3t7UatWLTF79mydpWRSUlLErFmzRKlSpYSJiYnIly+f8Pb2FhcuXJDqABADBgxI874fftaZOab0ztOVK1eEp6enMDMzEwUKFBCTJ08Wq1atSrNtZs59Rj6H4OBg8f333wsHBwdhYmIiChYsKPr06SMiIyM/8an/n7dv34rZs2eLqlWrCqVSKUxMTETx4sXFoEGDdJaIEeLLfjcy+v1KXSJk8+bNwt/fXzg4OAhzc3PRtGlTnSVMhBDi+vXrwsvLSyiVSmFvby969eolLWXz/u/1x9479bX3lzvJ6Od59+5d0bp1a2FjYyPMzMxEtWrVREhIiE6d1GPZvn27TvnHvntkOGRC8ApKotxu7dq16NatG/7++2+dxXEp70r9nbh///5n3+s3rzl+/Djq16+P7du3o3Xr1vpuDtFnYZ8tERERkYFgsCMiIiIyEAx2RERERAaC19gRERERGQj22BEREREZCAY7IiIiIgPBBYpzOa1Wi4iICFhZWWX49k1ERESUewgh8ObNG+TPn/8/F6FmsMvlIiIi4OLiou9mEBERURZ7/PgxnJ2dP1mHwS6Xs7KyAvDuZKtUKj23hoiIiL622NhYuLi4SH/zP4XBLpdLHX5VqVQMdkRERAYsI5dccfIEERERkYFgsCMiIiIyEAx2RERERAaC19gREdFHaTQaJCcn67sZRAbN2NgYCoXiq+yLwY6IiNIQQiAqKgqvX7/Wd1OI8gQbGxs4Ojp+8Zq0DHZERJRGaqhzcHCAhYUFF0AnyiJCCLx9+xbPnz8HADg5OX3R/hjsiIhIh0ajkUKdnZ2dvptDZPDMzc0BAM+fP4eDg8MXDcty8gQREelIvabOwsJCzy0hyjtSv29fek0rgx0REaWLw69E2edrfd8Y7IiIiIgMBIMdERGRHtSrVw9Dhw7VdzPIwHDyBBERZVihMfuy9f0eTG/6WdtFRUVh6tSp2LdvH54+fQoHBwe4u7tj4MCB6NixI0aMGIExY8ak2W7y5MlYtGgRnjx5AmNjYwBAoUKF8PDhw4++l6+vL9auXZvpNu7YsUN6jxxngnU2v1/MZ22WG85z6r6HDh2aLUGewY6IiAzKgwcPULt2bdjY2GDWrFlwc3NDcnIyDh48iCFDhqBTp05Ys2ZNmj/4QgisXbsWXbp00Qlcf//9NzQaDQDg9OnTaNWqFW7dugWVSgXg/2Y0pkpOTs5QYLO1tf3SQ83T9H2ecyoOxRIRkUHp378/ZDIZzp8/j1atWqFEiRIoW7Yshg0bhrNnz6JHjx64ffs2Tp06pbPdiRMncO/ePfTo0UOnPF++fHB0dISjo6MUxhwcHODo6IiEhATY2Nhg69at8PT0hJmZGTZu3Ijo6Gi0b98eBQoUgIWFBdzc3LB582ad/X44FFuoUCFMmzYN3bt3h5WVFQoWLIhff/01az4kA5Cd59nR0RHHjx9HpUqVYGZmhiJFimDixIlISUkB8C4sTpgwAQULFoSpqSny58+PwYMHA3h3nh8+fAg/Pz/IZLIsn5TEYEdERAbj5cuX+P333zFgwABYWlqmed3GxgZubm6oWrUqVq9erfPamjVrUKtWLZQqVSrT7ztmzBgMGTIEN27cQKNGjZCQkIDKlStj3759uHbtGnr37o3OnTvj/Pnzn9zPnDlzUKVKFVy8eBH9+/dHv379cOvWrUy3x9Bl93k+efIkunTpgiFDhuD69etYvnw51q5di6lTpwIAfvvtN8ydOxfLly9HeHg4du3aBTc3NwDvhtydnZ0xadIkREZGIjIy8guO/L8x2BERkcG4c+cOhBD/+Ue7R48e2L59O9RqNQDgzZs3CA4ORvfu3T/rfYcOHYqWLVuicOHCcHJyQoECBTBixAi4u7ujSJEiGDRoEBo3boxt27Z9cj9NmjRB//79UaxYMYwePRr29vY4duzYZ7XJkGX3eZ44cSLGjBkDX19fFClSBA0bNsTkyZOxfPlyAMCjR4/g6OgILy8vFCxYENWqVUOvXr0AvBtyVygUsLKyknr/shKDHRERGQwhRIbqtW/fHhqNRgpaW7duhVwuR7t27QAAZcuWhVKphFKphLe393/ur0qVKjrPNRoNJk+eDDc3N9ja2kKpVOLgwYN49OjRJ/dTvnx56WeZTAZHR0fpVlP0f7L7PF++fBmTJk2S6iqVSvTq1QuRkZF4+/Yt2rRpg/j4eBQpUgS9evXCzp07pWHa7MbJE0REZDCKFy8OmUyGmzdvfrKeSqVC69atsWbNGnTv3h1r1qxB27ZtoVQqAQD79++X7gCQkYvmPxwOnDVrFubPn4958+bBzc0NlpaWGDp0KJKSkj65nw8nXchkMmi12v98/7wmu8+zWq3GxIkT0bJlyzSvmZmZwcXFBbdu3cIff/yBw4cPo3///pg1axZOnDiR7TOfGeyIiMhg2NraolGjRli8eDEGDx6cJnC9fv0aNjY2AN4N09WrVw8hISE4ffo0Zs2aJdVzdXX9onb89ddf8PHxQadOnQAAWq0Wt2/fRpkyZb5ov/ROdp/nSpUq4datWyhWrNhH65ibm6N58+Zo3rw5BgwYgFKlSuHq1auoVKkSTExMpBm3WY1DsUREZFAWL14MjUaDatWq4bfffkN4eDhu3LiBBQsWoGbNmlK9unXrolixYujSpQtKlSqFWrVqfbU2FC9eHIcPH8bp06dx48YN9OnTB8+ePftq+6fsPc8///wz1q9fj4kTJyIsLAw3btzAli1bMH78eADA2rVrsWrVKly7dg337t3Dhg0bYG5uLgXHQoUK4c8//8TTp0/x77//fp0P4CMY7IiIyKAUKVIEoaGhqF+/PoYPH45y5cqhYcOGOHLkCJYuXSrVk8lk6N69O169evXZkyY+Zvz48ahUqRIaNWqEevXqwdHRET/88MNXfY+8LjvPc6NGjRASEoJDhw6hatWqqFGjBubOnSsFNxsbG6xYsQK1a9dG+fLl8ccff2Dv3r2ws7MDAEyaNAkPHjxA0aJFkS9fvi8/+E+QiYxegUg5UmxsLKytrRETEyMtokhE9CUSEhJw//59FC5cGGZmZvpuDlGe8KnvXWb+1rPHjoiIiMhAMNgRERERGQgGOyIiIiIDwWBHREREZCAY7IiIiIgMBIMdERGli3c8IMo+X+v7xjtPEBGRDhMTE8jlckRERCBfvnwwMTGBTCbTd7OIDJIQAklJSXjx4gXkcjlMTEy+aH8MdkREpEMul6Nw4cKIjIxERESEvptDlCdYWFigYMGCkMu/bDCVwY6IiNIwMTFBwYIFkZKSkm33uCTKqxQKBYyMjL5KzziDHRERpUsmk8HY2BjGxsb6bgoRZRAnTxAREREZCAY7IiIiIgPBYEdERERkIBjsiIiIiAwEgx0RERGRgWCwIyIiIjIQDHZEREREBoLBjoiIiMhAMNgRERERGQgGOyIiIiIDwWBHREREZCAY7IiIiIgMBIMdERERkYFgsCMiIiIyEAx2RERERAaCwY6IiIjIQDDYERERERkIBjsiIiIiA8FgR0RERGQgGOyIiIiIDISRvhtAX0mgM2Aq03criIjI0EyI0XcLKBPYY0dERERkIBjsiIiIiAwEgx0RERGRgWCwIyIiIjIQDHZEREREBoLBjoiIiL6KFy9eoGnTprC0tETJkiVx5MiRdOuVLVsWSqVSeigUCgwaNEh6PSoqCq1bt4a1tTXs7Ozg7+8vvXbp0iXUrl0bKpUKRYoUwcqVK7P8uHITBjsiIiL6KgYMGABHR0e8ePECs2bNQtu2bfHy5cs09cLCwqBWq6FWqxEdHQ2VSoVWrVpJr7do0QK1atVCZGQknjx5gg4dOkivde7cGY0aNcLr168RHBwMPz8/3LhxI1uOLzdgsCMiIqIvplarsWvXLkycOBEWFhZo0aIF3NzcsHv37k9ut3fvXqhUKnh6egIADhw4AFNTUwwbNgwWFhYwNzeHm5ubVP/Bgwdo37495HI5KlWqhNKlS+PmzZtZemy5CYMdERERfbHw8HAolUo4OztLZW5ubggLC/vkdkFBQejUqRNksneL7J8/fx6FChWCt7c37O3t0aBBA50euUGDBmHDhg1ISUnB+fPn8ejRI9SoUSNrDioXYrAjIiKiL6ZWq6FSqXTKVCoV1Gr1R7eJjo7GgQMH0LlzZ6ns6dOn2LJlCwYPHoyIiAg0btwYP/zwAzQaDQDA29sb69evh5mZGWrVqoUZM2bAyckpaw4qF2KwIyIioi+mVCoRGxurUxYbGwulUvnRbbZs2QJ3d3eUKlVKKjM3N0edOnXg7e0NExMTjBgxAlFRUbhz5w5evnyJpk2bYtasWUhMTERoaCj8/f0RGhqaZceV2zDYERER0RcrXrw41Go1nj59KpVdu3YNZcuW/eg2QUFBOr11AFCuXDlpWPZDd+/ehaWlJVq3bg2FQoHy5cujVq1aOHHixNc5CAPAYEdERERfTKlUwsfHBwEBAYiPj0dISAiuXLkCHx+fdOuHh4cjNDQU7du31ylv2bIlLl++jD/++AMajQbz5s2Do6MjihUrhhIlSuDt27fYvXs3hBC4fv06Tp48qTO5Iq9jsCMiIqKvYsmSJYiIiICdnR2GDRuGrVu3wtbWFhs3bkzTcxcUFITGjRvD3t5ep9zOzg7BwcEYNGgQbGxssHPnTuzcuRMKhQLW1tbYtm0bAgICoFKp4O3tjWHDhsHLyys7DzNHkwkhhL4bQZ8vNjYW1tbWiBljBZVp+l3XREREn21CjL5bkOdJf+tjYtJMUPmQUTa1iYiIiHKwuKSP9PPExX10G0tLyyxqDX0uBjsiIiKCMvBN+i8EfnxWKwf9ch5eY0dERERkINhjR0RERFD7W6X/wrjI7G0IfREGOyIiIoKlyUcm4PE6ulyFQ7FEREREBoLBjoiIiMhAMNgRERERGQgGOyIiIiIDwWBHREREZCAY7IiIiIgMBIMdERERkYFgsCMiIiIyEAx2RERERAaCwY6IiIjIQDDYZTOtVqvvJhAREZGBYrDLRkIIyOXvPvKZM2fi6NGjem4RERERGRIGu2yi1Wohk727wfKaNWswd+5cmJub67lVREREZEgY7LJJak/d6dOnce7cOUyfPh01a9bUc6uIiIjIkBjpuwF5yYkTJ9CjRw+8fv0adevWBfBueDa1Jy8jEhMTkZiYKD2PjY396u0kIiKi3InBLgt9GNo8PT3h6+uLhQsXYtu2bahXrx7y58+fqXAXGBiIiRMnpikvl7AKcmHx1dpOREQEABizL8NVH0xvmoUNoYzgUGwWef+aOq1Wi7i4OADATz/9BD8/P9y9excLFy7Es2fPIJPJIITI0H79/f0RExMjPR4/fpxlx0BERES5C3vssoBWq5WuqZs/fz5OnDiB58+fo0qVKpg6dSr8/f2RlJSE3bt3QyaTYciQIfj2228ztG9TU1OYmppmZfOJiIgol2KwywKpoc7f3x/r1q3D4MGDUbJkSbRq1QqRkZEICgpCQEAAhBDYt28fYmNjMWnSJNja2uq55URERJSbMdhlkcuXL2Pnzp3YtGkT6tWrhz///BOmpqZo2LAhTExMAAATJkzA69evERMTg2+++UbPLSYiIqLcjsHuK9FoNFAoFNLz169fw8zMDPXq1cPOnTvRpUsXzJs3Dz179kRMTAxOnDiBFi1aYN68edLkiczOkCUiIiJ6HydPfAWvXr2SQt3ly5cBQJrtOnXqVHTt2hWzZs1Cnz59AABXr17FnDlzcOXKFQBgqCMiojzpxYsXaNq0KSwtLVGyZEkcOXIk3Xply5aFUqmUHgqFAoMGDZJeX7BgAVxdXWFtbQ1PT0/cuHFDeu3IkSNwd3eHlZUVypQpgz179mT5cekTg90XOnToEPz8/BAVFYXBgwfDy8sL0dHRsLOzQ5kyZTBt2jT07t0bffv2BfBuHbqZM2ciX758KFeunLQfhjoiIsprBgwYAEdHR7x48QKzZs1C27Zt8fLlyzT1wsLCoFaroVarER0dDZVKhVatWgEA/vnnH4wfPx4hISF4+fIlPDw80L17dwDvRtNat24NPz8/xMbG4pdffkH79u3x6tWrbD3O7MSh2C/05MkTXLx4EY0bN8aTJ09w+vRp2NnZAQB69uyJ+/fv4++//8bs2bNhYWGBnTt3IioqCqGhoZDL5TozaImIiPIKtVqNXbt24d69e7CwsECLFi3g5uaG3bt3o1u3bh/dbu/evVCpVPD09AQAPHz4EG5ubnBzcwMAdOjQAQsXLgTw7rKo169fo3379pDJZGjcuDEsLS3x4MEDg722nYniM6WuO9e9e3eUKVMGV65cgaenpzQxAgAaNGiASZMmoUKFCvjll1+we/duuLi44OLFizA2NkZKSgpDHRER5Unh4eFQKpVwdnaWytzc3BAWFvbJ7YKCgtCpUydppKthw4ZITEzExYsXkZycjKCgIHz//fcAADs7O7Rv3x5BQUHQaDQICQmBubk5SpcunXUHpmfssfsMH14P5+HhgbJly2LPnj2YMmUK/Pz8ULZsWQDA999/j++//x6TJk2CtbW1tE1KSgqMjPjxExFR3qRWq6FSqXTKVCoVoqOjP7pNdHQ0Dhw4gBkzZkhlSqUSLVq0QNWqVQEALi4uOHnypPR627Zt0bNnT/Tp0wcmJibYvXs3zMzMvvLR5BzsLvoMqaFu5cqVmDJlCvr374/x48ejV69euHjxIubOnYvr169L9U+ePKkT6oQQDHVERJSnKZXKNPc7j42NhVKp/Og2W7Zsgbu7O0qVKiWVrVy5Elu3bsWdO3cQHx+PAQMGwMfHBwBw48YNdO7cGTt37kRSUhL279+Pjh074unTp1lzUDkAg91nUqvVuHjxIrZu3YpJkyYBAHr16oV+/frh8uXLCAwMxP79++Ht7Y2+ffvq3DKMEyWIiCivK168ONRqtU7IunbtmjTilZ6goCB07txZp+zy5cto0aIFChUqBGNjY/Tt2xehoaF49eoVrl27hnLlysHDwwNyuRz16tVDwYIFcf78+Sw7Ln1jsMsgrVar81ypVGLcuHFo1qwZdu3ahQkTJgB4N2Fi0KBBePr0KYYMGYK3b9/i0qVLDHNERETvUSqV8PHxQUBAAOLj4xESEoIrV65IvW0fCg8PR2hoKNq3b69TXqVKFYSEhODp06fQaDRYsWIFChQogG+++Qbu7u4ICwvDmTNnAACnTp3CzZs3UaZMmSw/Pn3heGAGpU5yuHz5MipUqADg3Vp1gwYNglarxZ49e6BQKPDTTz+hS5cu8PDwQFJSEooXLw65XM5r6oiIiD6wZMkS+Pr6ws7ODs7Ozti6dStsbW2xceNGTJs2TWciRVBQEBo3bgx7e3udffj6+uLGjRuoWrUq4uLiULp0aQQHBwN41yu4ZMkSdO3aFREREXB0dMSiRYtQsmTJbD3O7CQT748R0ieFhIRg1KhRGDp0KHr37i2VP3nyBBMmTMDvv/8OPz8/DB8+XGe7rFzSJDY2FtbW1nAZug1yU4sseQ8iIqKMeDC9qb6bYJBS/9bHxMSkmXDyIQ7FZkKhQoVQuXJlBAUFYeXKlVK5s7MzevXqhbdv32LGjBlYtWqVznZc0oSIiAyVNilBesTFxSEuLk7fTcrTODb4Een1spUrVw7jxo3D9OnTsWrVKggh0KtXLwCAsbExGjVqhAYNGnxyYUUiIiJD8nhua+ln5dx3/+VgoP4w2KVDCCGFuhUrVuDOnTtQq9Xo1q0bqlSpggkTJmDixIlYuXIlHj58iLZt22L8+PEoVKgQevToAZlMBo1GI90/loiIiCg78Bq7D7zfUzdy5EisXLkSNWrUwMuXL3H58mVMmjQJfn5+ePr0KVatWoXly5fDysoKDg4O+PPPP2FsbJxmAeOsxGvsiIhIn7RJCdLPNyY3BgBYWlrqqzkGKTPX2LHH7gOpoS4qKgrPnj3DH3/8gcqVKwMApk6dihkzZsDKygr9+vXD6NGj0b9/f0RERKBixYqc/UpERHmO3OT/7uLAQKd/TCDpWLNmDYYPHw4XFxfY2tpKPXDjxo1DfHw8xo4di1atWsHBwQFKpRJOTk4A3vX2MdQRERGRvnC65geEEHB0dESFChVw//59AO/uFPH27VsAwMCBA2Fqaop//vknzbac/UpERET6lOeTyId3lJDJZPDy8sJPP/0EV1dXNG/eHPHx8bCweHf9WkJCAoyMjGBsbKyP5hIRERF9VJ4Odu9PlNi/fz927dqF8+fPw9jYGJ6enli4cCEAoGLFiti2bRt27tyJgQMHwsHBAd99950+m05ERESURp4Odqmhzt/fH23btoW/vz9q166NBQsWQKFQoG7duli0aBEsLCzw448/Yu/evfDw8MCpU6egUCig0Wj0fARERERE/ydPXumfOhlCCIFHjx7h5MmTOHbsGPLly4eQkBAMGTIEb968wbhx41C3bl3MmjULkyZNwuXLl6WgFx8fD3Nzc30fChEREZEkzwW794dfX79+DbVajVq1aqFixYowMjLCwIEDYWJign79+kEmk2Hs2LGoV68eZDIZRo4cCS8vLxw6dAhKpVLPR0JERESkK88NxaaGunHjxqFx48Zo1KgRfv/9dzx8+FCq07t3byxduhQTJkzAmDFjoFAoUK9ePcyZMwfPnj2Dj4+PvppPRERE9FF5psfu/Z669evXY/369Rg8eDD+/fdfzJo1C0uWLIG/vz/s7e0BvAt3cXFx2LFjh3R7MA8PD6xduxbOzs76PBQiIiKidOWZYJca6k6fPo3Lly9jxowZ6NChAwDA3d0dHTt2hEKhwKhRo6Rw5+fnh6FDh+rc+9XDw0Nvx0BERET0KQYf7O7fv49ChQpBJpPh8uXLaNCgATQaDWbMmCHVad++PQBI4c7Pzw8ODg4AIE2yUCgUemk/ERERUUYZ9DV2Z86cQdu2bbFs2TIIIVChQgVs2LABdnZ2+PPPP3Hjxg2pbvv27bF582bMmDEDwcHBOvuRyWTZ3XQiIiKiTDPoHruiRYvCyckJW7duhVwuR+/evdGqVSskJydjxIgRWLJkCQYNGoQSJUoAANq1awc7OzvUq1dPvw0nIiIi+gwG22On1Wrh4OCA1atXw8HBAUFBQfj1118hhMCPP/6IGTNmYOfOnVi4cCFu374tbefl5QUjIyOkpKTosfVEREREmScTQgh9NyKrpM6EffHiBQYMGICIiAh07twZvXv3hkwmw8aNGzF27FjUrVsX06ZNg4uLi76bnGmxsbGwtrZGTEwMVCqVvptDREREX1lm/tYbbI8d8G4mrFarRb58+bB48WLkz59fp+euY8eOGD9+PN68eYMCBQrou7lEREREX8Sge+xSfdhzFxUVhU6dOqFnz57SMijv18tN2GNHRERk2PJcj93Hsmlq+Yc9dwUKFMAvv/yCvXv3pqlHRERElFvl+iSj1Wql5UhevXqFZ8+eSRMfZDIZtFotAN1wN3/+fLRt2xbNmjWT6hERERHldrl6KFYIIYWySZMm4eTJkwgNDcX//vc/1KxZEz169EizTeodJD72PLfhUCwREZFhyzNDsamh7ueff8aCBQswcOBABAcH4/79+5g0aRLu3LmTZpsPQ1xuDnVERERE78vVwQ4AHjx4gEOHDmHz5s3w8fGBEAJnz55FQEAAihUrxvXoiIiIKM/IdcEu9Zq5VEZGRnj16hXc3d2xa9cu+Pj4YM6cOejevTsSEhKwceNGhIeH66m1RERERNkn1wW71JmrFy5cQHx8PFJSUmBsbIy5c+eie/fumDFjBvr27QsAuHHjBnbu3ImIiAh9NpmIiIgoW+S6YCeEwOHDh+Hp6Ym4uDgUKlQIrVq1wvTp0+Hr64v+/fsDAOLi4vDTTz8hISEBHh4eem41ERERUdYz0ncDMksmk6Fhw4YoXrw4xo4di19//RVjx47Fs2fPsHDhQiQnJyM5ORnh4eF48eIFQkNDpaVOuE4dERERGbIcn3Q0Go3O86SkJABA9+7dcfv2bTx48ACmpqZYvHgx5s2bh8jISLx69Qp16tTBxYsXYWxsjJSUFIY6IiIiMng5dh27x48fw8XFRXp+5coVlC9fXnr+5MkTVK5cGf3790dAQIBUnpycDGNjY+l5bl+n7r9wHTsiIiLDluvXsfvhhx+wadMm6fnOnTvRsmVLNGnSBFevXsWLFy/g7OyMiRMnYvv27bh06ZJU18hId3TZkEMdERER0ftyZLDr3bs3/Pz8AAAJCQnw9PTEggULEBcXh3bt2qFHjx74888/UbVqVdjY2ODatWsAdG8vRkRERJTX5LhgJ4RAkyZNYGJignnz5qF3796IjY1FkyZNcOLECfz0009wcHDA999/jw0bNiA8PBxjx45FfHw8r6MjIiKiPC3HJaHUHjetVgtHR0ccPHgQ8+bNw/Xr1wEA7du3x8qVK7F7924kJCTAyMgI5ubmMDU11WeziYiIiPQuxyx3sn//fri7uyN//vwYN24cLCwsMG7cOAghMHLkSGi1WgwYMAAlS5YEADRq1AgeHh7w9/dHgQIFuKQJERER5Xk5Itj9+++/CAwMxMOHD9G4cWOsX78eZ8+eBfCuh04IgVGjRgEABg0ahOLFiwMAzM3NUbBgQQCGP/uViIiI6L/kiGBnb2+PFStWoF69eli/fj327NkDd3d3JCQkwMzMDB06dAAAjB49GnK5HH369EHp0qV1Jkow1BEREVFep/dgJ4SATCaDXC5HgQIF4OzsjEGDBuHw4cMoWLAgkpKSYGJigg4dOkAmk6FTp05wdXVF6dKl9d10IiIiohxFbwsUf3g9XEpKCuLj43Hv3j34+fnh8ePHOHLkiDTUmurUqVOoWbMme+j+Py5QTEREZNhy/ALF74e6q1ev4saNG7hz5w6srKxQvnx5TJs2DQULFkTDhg3x8OFDAEDHjh0xb9481KlTBwqFIs2txoiIiIjyumzvsUsdegWAgIAABAcH4+3btzA2NsbIkSPRq1cvAMC5c+cwfvx4/PXXX3B3d0dERATCw8N1bhdG7LEjIiIydDm6xy411E2cOBFLly7FggULcPToUdSuXRt9+vTB3LlzAQDVq1fHmjVrMHPmTDRr1gx37tyBsbExe+qIiIiIPiLbJk+831N36dIlHDt2DJs3b0aDBg2wb98+7Nq1Cz4+Phg+fDjkcjmGDBkCZ2dnDBw4UNoHlzQhIiIi+rhs6bF7P9TdvHkTZcqUQdOmTVGnTh0cO3YMvXr1QmBgIDZt2gRvb2/4+flh6tSpafbDUEdERET0cVke7N4Pdf7+/ujQoQOSk5MxYMAAmJqaYtOmTWjRogW6d+8Oc3NzFClSBNWrV8ehQ4egpwm7RERERLlSlge71FB3/vx5XLhwAUuWLIGlpSUsLCyQkJCAixcvwszMDCYmJoiPj8fTp0/h7++PEydOQCaTMdwRERERZVC2XGO3YcMG7NixA6ampqhcuTK0Wi1kMhnMzMzQrl07+Pv7IyYmBmFhYUhOTkbTpk0B6Pb2EREREdGnZUuwCw8Px5kzZ6BQKBAREQFXV1epJ87X1xdGRkY4cuQIKlWqhIULF0rr1PGaukwIdAZMGYKJiOgLTIjRdwvoC331dew+vKNEqkWLFmHOnDlo1KgR/P394erqqvN6cnKytEZdSkoKjIz0frezXEFa22aMFVQMdkRE9CUY7HKkzKxj91XT0/uh7siRI0hISEBsbCzat2+PgQMHIjk5GevXr8eCBQswZMgQFCxYUOq5Sw11QgiGOiIiIqLP8FUTVGqoGzNmDLZv3w47OztERkZi0aJFWL16Nfz8/JCcnIytW7dCLpejf//+KFy4sM4+eE0dERER0ef56rNilyxZgtWrVyM4OBjnz5/H1KlTcebMGTx+/BgAMGrUKLRr1w6bN29GSEjI1357IiIiojzri3vsPpy5euvWLQwZMgQVK1bE1q1bMXjwYCxZsgReXl548+YNrKysMGrUKOTPnx/t27f/0rcnIiIiov/vi3vs3g91Wq0WoaGhkMvlOH36NHr27Inp06ejb9++0Gq1CAwMxMqVKwEAnTp1kma/EhERUe7y4sULNG3aFJaWlihZsiSOHDmSbr2yZctCqVRKD4VCgUGDBgEAHjx4AJlMpvP6xo0bpW0vXbqE2rVrQ6VSoUiRIlKGoI/77B67c+fOwcTEBBUrVsTQoUNRpUoVdOrUCd27d8eCBQsQEBCAZcuWoXv37gCAN2/e4NKlSzA1NdXZD5c0ISIiyn0GDBgAR0dHvHjxAn/88Qfatm2L8PBw2Nra6tQLCwuTfk5MTISjoyNatWollSkUCqjV6nTfo3PnzmjTpg1OnjyJS5cuwdPTE7Vr10bp0qWz5qAMQKaDnVarxePHj9GrVy9pseFNmzbhwoULAICKFSsiX758qFChAgoWLAgAuH//PgYOHIjo6GiMGzfu6x4BERERZSu1Wo1du3bh3r17sLCwQIsWLeDm5obdu3ejW7duH91u7969UKlU8PT0zND7PHjwAO3bt4dcLkelSpVQunRp3Lx5k8HuEzI9FCuXy+Hq6orJkyfj4MGD2Lx5MzZs2IDy5ctDCAF3d3eMHj0adnZ26NKlCwoVKoSWLVvi1atXOHXqFIyMjDj8SkRElIuFh4dDqVTC2dlZKnNzc9PpnUtPUFAQOnXqpHMZl0ajQf78+eHq6oqhQ4fi7du30muDBg3Chg0bkJKSgvPnz+PRo0eoUaPG1z8gA5KpHrsjR47g1KlTCAgIQKFChWBjYwMbGxscPnwYRYsWRZUqVQAADRo0QNGiRREREYGwsDAUKVIE9erVg0Kh4OLDREREuZxarU6zUK5KpUJ0dPRHt4mOjsaBAwcwY8YMqcze3h4XLlxAhQoV8OTJE/j6+mL06NFYuHAhAMDb2xtdunTB1KlTAQCrVq2Ck5NTFhyR4chwj11iYiK2bduGHTt2YN68eahQoQKuX7+OKVOm4NKlS1iwYIE0HAsAhQoVQq1atdCrVy80aNBAmijBUEdERJS7KZVKxMbG6pTFxsZCqVR+dJstW7bA3d0dpUqV0tlPpUqVoFAo4OrqiunTp2PHjh0AgJcvX6Jp06aYNWsWEhMTERoaCn9/f4SGhmbNQRmIDAc7U1NTBAQEoH79+ti4cSOmT58OAGjZsiWGDx+OGzduYPHixfj7778BAM2aNZNOTipOlCAiIsr9ihcvDrVajadPn0pl165dQ9myZT+6TVBQEDp37vzJ/crlcumOVHfv3oWlpSVat24NhUKB8uXLo1atWjhx4sTXOQgDlalr7PLnz48xY8agRo0a2LVrF6ZNmwYAaN++PYYNG4abN2+ib9++qFy5Mq5evYrmzZtnSaOJiIhIf5RKJXx8fBAQEID4+HiEhITgypUr8PHxSbd+eHg4QkND06xfe/78eYSHh0MIgYiICPj7+6NFixYAgBIlSuDt27fYvXs3hBC4fv06Tp48CTc3tyw/vtws05MnHB0dMW7cOFStWhV79uzRCXcTJkxAx44d0ahRI9y9exfGxsZISUn56o0mIiIi/VqyZAkiIiJgZ2eHYcOGYevWrbC1tcXGjRvT9NwFBQWhcePGsLe31ym/c+cOGjZsCKVSiWrVqqFMmTKYPXs2AMDa2hrbtm1DQEAAVCoVvL29MWzYMHh5eWXbMeZGMpHa55lJUVFRmDp1Kv7++2/4+PjA398/TR2NRsPh1ywWGxsLa2trxIyxgsqU99klIqIvMCFG3y2gdEh/62Ni0kxa+dBnz2RI7bmbNm0aQkJCoFarpVkrqRjqiIiI9CMu6TP6beLiMr2JpaVl5t+HsswXTVF1dHTE2LFjMWrUKLx48SLNfWOJiIhIP5SBbzK/UeDHZ7V+zGcO/FEW+eK1RxwdHTFv3jzY2NhAJpMx3BERERHpyVdZVC71vnBarRZyeabnYxAREdFXpva3yvxG4yK/fkMoW33V1YIZ6oiIiHIGS5PPGD3j9XK5HpMYERERkYFgsCMiIiIyEAx2RERERAaCwY6IiIjIQDDYERERERkIBjsiIiIiA8FgR0RERGQgGOyIiIiIDASDHREREZGBYLAjIiIiMhAMdtlMq9UCAIQQem4JERERGRoGu2wkhJDup3v+/Hk9t4aIiIgMDYNdNtFqtZDJ3t2QedSoUahZsyaePn2q51YRERGRIWGwyyapPXU3btzA27dvceLECRQoUEDPrSIiIiJDwmCXjYKDg9GoUSMcP34chQsX/qzr7BITExEbG6vzICIiIgIAI303IC9RKBQoW7YsTpw4gejoaDg7O0Or1Uq9eRkRGBiIiRMnpikvl7AKcmHxNZtLRER5zZh96RY/mN40mxtCn4s9dlkkdfbr+/73v/9h9OjRqFChAjp37oywsDDI5fJ0636Mv78/YmJipMfjx4+/ZrOJiIgoF2OwywLv98Lt2LEDy5cvx8yZM/H06VPUq1cPM2bMQP78+dGzZ09cv349U+HO1NQUKpVK50FEREQEMNhlidRQN2rUKAwaNAiHDx/G2rVr4e3tjY0bN6Ju3boYOnQobG1t0bt3b1y5ciVTw7FERERE6WGayCIbN27Epk2bsG/fPgQHB2Py5Mm4du0arKysAACNGzeGn58fkpKSsHjxYj23loiIiAwBg91X8uEM1wcPHqBBgwZwd3fH5s2b0b17dyxevBgtWrSAWq3Gs2fP4OXlhYULF2Lp0qV6ajUREdHX9eLFCzRt2hSWlpYoWbIkjhw5km69smXLQqlUSg+FQoFBgwZJr0dFRaF169awtraGnZ0d/P39pdfCwsJQt25dqFQqlClTBsePH8/qw8o1GOy+ktTFh58/fw4AePr0Kezs7HDx4kX07t0b06dPR79+/SCEwPr167FhwwYIIVC9evVMT6AgIiLKqQYMGABHR0e8ePECs2bNQtu2bfHy5cs09cLCwqBWq6FWqxEdHQ2VSoVWrVpJr7do0QK1atVCZGQknjx5gg4dOgAAkpOT4ePjg9atW+PVq1dYsGABWrdujejo6Gw7xpyMwe4L/f777wgJCQEADB8+HAsXLgQAtG7dGr/++isqV66M5cuXo1+/fgCA+Ph47N27F0+fPpXCIABeY0dERLmeWq3Grl27MHHiRFhYWKBFixZwc3PD7t27P7nd3r17oVKp4OnpCQA4cOAATE1NMWzYMFhYWMDc3Bxubm4AgFu3buHVq1cYPHgwFAoFvLy8ULFiRezcuTPLjy83YJr4AtHR0Vi/fj2GDRuG1q1bY/HixWjTpg0AoHLlyhgyZAicnJzw9u1bvH79GpcvX0arVq0QFRWFmTNn6rn1REREX1d4eDiUSiWcnZ2lMjc3N4SFhX1yu6CgIHTq1Enq8Dh//jwKFSoEb29v2Nvbo0GDBrhx44ZU/8PLn4QQ//keeQWD3Rews7NDQEAAZDIZdu7ciTlz5qB8+fIQQsDa2hqdOnVC586dMWTIEJQuXRpdunRBUlISzp8/DyMjI2g0Gn0fAhER0VejVqvTLMOlUqmgVqs/uk10dDQOHDiAzp07S2VPnz7Fli1bMHjwYERERKBx48b44YcfoNFoULJkSdjY2OCXX35BcnIyfv/9d5w4cQJxcXFZdly5CYPdZ3j/ejilUokiRYqgcePGWL58Ofbu3Sv9i6N06dKYNm0arl69iqCgIKxduxaHDx+GsbExUlJSoFAo9HUIREREX51SqUxzq8vY2FgolcqPbrNlyxa4u7ujVKlSUpm5uTnq1KkDb29vmJiYYMSIEYiKisKdO3dgbGyMXbt2Yffu3XB0dMTcuXPRrl07nV7CvIzB7jO8v/hwgQIFcODAAUyZMgXu7u4YM2aMdM0d8G5Sha2trXQNQOpECSMj3s2NiIgMS/HixaFWq/H06VOp7Nq1ayhbtuxHtwkKCtLprQOAcuXK6VyH/qHy5ctLt+c8ePAg7t27h2rVqn35ARgABrvPdPfuXbRu3RpNm767f17FihXRr18/VK5cGWPHjpUuFP3hhx/w66+/6mzLiRJERGSIlEolfHx8EBAQgPj4eISEhODKlSvw8fFJt354eDhCQ0PRvn17nfKWLVvi8uXL+OOPP6DRaDBv3jw4OjqiWLFiAIArV64gISEBb9++xaxZs6DVatG4ceMsP77cgAkjgz68ULNgwYLYu3cvLl68iObNmwMAatasif79+6NatWro2LEjKlSogLCwMPj5+emjyURERNluyZIliIiIgJ2dHYYNG4atW7fC1tYWGzduTNNzFxQUhMaNG8Pe3l6n3M7ODsHBwRg0aBBsbGywc+dO7Ny5U7qEac2aNXB0dISjoyPOnj2LXbt2Zdfh5Xgy8WFioQxLSUnBoUOH0LVrV1SvXh179+4F8K43LywsDHfv3sWgQYNgZGSElJSULBl+jY2NhbW1NVyGboPc1OKr75+IiOjB9Kb6bkKelvq3PiYm5j/vEc8LvTJh3rx5CA0Nxfr16wEARkZG+P7777F27Vp06NAB7dq1w9atW1G0aFEULVpU2k6j0fCaOiIiylW0SQnSz6kzTi0tLfXVHMogDsVmUFJSEoyNjRESEqJzyxMjIyM0atQI3bp1w/bt2+Hl5ZVmW85+JSKi3Obx3NbSI/W2X5TzsRvpI7Rarc4kBxMTE3To0AHm5uYYPXo0tFotFi9eDOBdcCtSpAhatWoFIUSabYmIiIiyA4NdOt4PZmfPnsWbN29QqlQp5M+fH927d4dWq8XYsWMhhMCSJUvw+vVrnDp1Cp6enhg4cGCafRAREeU2Ln7B0s83JnPGaW7BYJeO1EA2evRorFixAmZmZnjz5g1+/PFHDBw4ED179oSZmRkGDx6MHTt2QKVSwdTUFBs3bgTwbgYtQx0REeVmchMz6WdeW5d7MH285/0JwkePHsX27dsRHByMK1euYNGiRQgPD8fEiRNx48YNdOrUCVevXsXo0aMxevRoXLx4UbpN2KcWVSQiIiLKKuyxe09qIFu0aBFevXqF1q1b47vvvgMA+Pr6wsbGBgEBAfjtt98wfvx4FChQQGeNOo1Gw4kSREREpDfssfuAVqvF3r17ERAQgKtXryIxMVF6zcfHB02aNMGyZcsQHx+fZluGOiIiItKnPB/stFqtznO5XI5du3ahW7duOHnyJE6cOKEzRFuuXDk4OjoiKSkpu5tKRERE9El5eij2/Zmrjx49glwuh7OzM8zNzbF8+XK8fPkSHTt2xNKlS1GpUiVYWVlh1apV+Oabb/5z5WciIiKi7Jang11qqPP398eePXsQERGBvn37wtfXF6VKlUJwcDDatm2Ltm3bwsnJCQ0bNkRiYiKOHTsGmUzGJU2IiIgoR8nzqWTnzp3Ytm0bfv75Z0ycOBGrV6/GlClTcOHCBSgUCmzduhXdu3dHZGQkOnbsiFOnTsHY2BgpKSkMdURERJSj5Lkeuw972aysrNC3b1+0a9cOAFChQgV069YNWq0WI0aMQKVKlbBs2TK8ePECnTp1wp49e1C9enXe+5WIiIhynDzV5fT+wsHLli3DgAEDMHnyZKjVaqmOp6cn1qxZg3PnzmHu3Lk4e/YsjIyMsH37dnh6eqJ27dr4+++/9XUIRERERB+VZ4KdEEJap27KlCnw8/PDs2fPcP78eezYsQOHDh2S6qaGu99++w2///47hBAwMTHBhg0b0L59e1hbW+vrMIiIiIg+Kk+MJ74f6s6dO4eoqCgcOnQIHh4eOHv2LIYPH45ly5ZBLpfDy8sLAFC3bl2cPn0abm5ukMlkUrgLCgrS56EQERERfVSe6LFLDXW7du1Cnz59cOLECRQpUgQAUKNGDQQGBuL58+dYvHgxjhw5Im3n7u4OhULB24QRERFRrmDQwe7w4cP48ccfpQWGra2t4erqivv37+PYsWNSvbp16yIwMBAvX77ExIkT8c8//+jsh3eUICIiotzAYIditVotbt26hUuXLqF79+5YtWoV6tevDysrK8jlcixduhQWFhZo2bIlAMDDwwM///wzgoODUalSJT23noiIiCjzDLbHTi6Xo1u3bhg2bBguX76MLl26QKvVokqVKhg5ciTy58+P+fPnY+fOndI2DRo0wNKlSyGXy9PcaoyIiIgop5OJ92+EakBSJ0y8ffsW69evx6+//ooyZcpg/fr1kMvl+OuvvzBv3jxER0ejZ8+e6NChg76b/FliY2NhbW2NmJgY3uaMiIjIAGXmb73B9tilzmS1sLBAly5d0Lt3b1y/fl3quatduzb8/Pwgk8nw119/6bu5RERERF/MYHrs4uPjYW5unqY8vZ67smXLYt26dZDL5bh69SrKli2ba28Pxh47IiIiw5bneux27twJf39/PH78OM1r6fXc3bx5E82bN4cQAm5ubrymjoiIiAyCQcyKff36NRYuXAgzMzMMHDgQzs7OOq9/GO7i4uJw7do1nYWLc2uPHREREVGqXBvsUkeQZTIZunXrBhMTE/Tr1w8ajQZDhgz5ZLgbOHAgTExMIJPJoNVqGeqIiIjIIOTqYPd+IOvYsSNSUlIwaNAgAPhouAMAU1PTdPdBRERElJvlymC3adMmLF26FL169YKzszO+++47CCHg6+sLuVyO/v37QwiBIUOGwMXF5aP74W3CiIiIyJDkumD34sULTJgwAXfu3IFGo8G9e/dQokQJFCtWDAMHDkT79u1hZ2eHDh06wNzcHD179oSrq6u+m01ERESU5XLdOKRSqcTSpUvh5uYGAPjzzz/x/fff486dO2jRogVcXV1x7do1lCpVCsuWLcPcuXPx/PlzPbeaiIiIKOvlmnXs9u7diypVqsDJyQkJCQk4e/Ys2rRpgyZNmmDdunUAgH/++QfXr19HcHAwoqOjcebMGXh6euLo0aMGO+zKdeyIiIgMW2b+1ueKYPfmzRs0bNgQN2/exM2bN+Ho6IikpCScPn0abdu2RbVq1RASEiLVT0xMhEajwcGDB9GiRQsoFAqdpU0MCYMdERGRYTO4BYqtrKywcuVKVK5cGZUqVUJUVBRMTExQq1YtbNu2DX///Td8fHyk+jKZDBYWFvjf//4HhUKBlJQUgwx1RERERO/LFcEOAMqVK4cFCxagePHiacLd1q1bcf78ebRs2RIAYGJiorOtkVGumyNCRERElGk5NtidPHkSmzdvhlqtRmJiIgCgTJkyWLFiBQoXLoxKlSohMjJSCndbtmzBrl27MGbMGD23nIiIiEg/cmRX1qlTp+Dp6QkAKF68OMqXLw9PT0989913KFOmDNasWYMRI0agYsWKuHz5Mr799lvUrFkTFy5cQPny5fXceiIiIiL9yHGTJzQaDW7evInhw4cjJiYGdnZ2qF69OpYvXw5TU1PY29ujadOmcHBwwPLly5GQkIATJ07AwcFBZx8KhUKPR5F9OHmCiIjIsOXayRNBQUGYN28eypYti59//hmurq5ISkpC7dq18eDBA+zatQvly5fHP//8g8GDB+P169e4desW/Pz8dPaTV0IdERER0ftyRI+dEAIajQZVq1ZFr1690L9/fwDAmTNnMHv2bDx//hwjR45EixYtpG3Onj2Lu3fv4q+//sL8+fNhbGysr+brFXvsiIiIDFuu67GTyWRISUnB69evYW5uLpXXrFkTI0eOhIODA3755Rf89ttv0ms1atRAx44dsWTJEhgbGyM5OVkfTSciIiLKMXJEsEtlZWWFb7/9FgCg1WoBvAtwI0eOhL29PRYtWoTdu3enu21e7bEjIiIiSqXXYHfkyBFMmTIFwLtgFhcXBysrKwDvhmdTR4lr1KiBESNGIF++fBg7diz+/PNPvbWZiIiIKKfS23IniYmJ2LZtG86ePQtLS0v06NEDiYmJ0pDqhxMgatSogWHDhmHPnj2oXbu2PppMRERElKPpdfJEREQEZs6ciXPnzqFWrVrYu3cvOnbsCCsrK2i1WpiamgIA4uPj8ezZM/Ts2ROlS5cGkLeWNPkUTp4gIiIybJn5W6/3WbGRkZGYOnUqjh8/juvXr6No0aKwsrJCXFwctFotZDIZzMzMYGNjg6NHj/L2YB9gsCMiIjJsmflbr/eU5OTkhPHjx0OhUMDGxgb16tWTrrtLSkqCkZER5HI5hBCQyWTsqSMiIiL6iBwxK9bR0RFjxoxBxYoVcejQIQQGBgIATExMoNFoALxbEkUIwVBHRERE9BE5ItgB73ruxo0bhxo1aiAkJATjx48HoLuMiUwm01fziIiIiHK8HBPsgHc9d2PHjkXRokXx/Plz5ICbYhARERHlGnq/xu5Djo6OmDdvHmxsbKThV/bUEREREf23HBfsAMDW1hbAu7tPyOU5qlORiIiIKMfKkcEuFUNdJgQ6A6bs2SQiogyYEKPvFlAWYXIiIiIiMhAMdkREREQGgsGOiIiIyEAw2BEREREZCAY7IiIiSteLFy/QtGlTWFpaomTJkjhy5Ei69cqWLQulUik9FAoFBg0alKZe3759IZPJ8OTJE6ns/e2USiXkcjnmzJmTZcdk6HL0rFgiIiLSnwEDBsDR0REvXrzAH3/8gbZt2yI8PFxalixVWFiY9HNiYiIcHR3RqlUrnTqhoaG4fv16mvdQq9XSzxEREShYsCBatmz5lY8k72CPHREREaWhVquxa9cuTJw4ERYWFmjRogXc3Nywe/fuT263d+9eqFQqeHp6SmVCCAwePBhz58795LabNm1CzZo1Ubhw4a9yDHkRgx0RERGlER4eDqVSCWdnZ6nMzc1Np3cuPUFBQejUqZPOXaPWrVuHUqVKoXLlyv+5bZcuXb6s4Xkch2KJiIgoDbVaDZVKpVOmUqkQHR390W2io6Nx4MABzJgxQyqLiYnBtGnTcOrUqU++35UrV3D79m20adPmyxqex7HHjoiIiNJQKpWIjY3VKYuNjYVSqfzoNlu2bIG7uztKlSollU2YMAF9+vSBg4PDJ98vKCgIzZs3h42NzRe1O69jsCMiIqI0ihcvDrVajadPn0pl165dQ9myZT+6TVBQEDp37qxTduzYMUyfPh2Ojo5wdHQEAFSqVAm///67VEer1WLTpk1ptqXM41AsERERpaFUKuHj44OAgAAsXLgQR44cwZUrV+Dj45Nu/fDwcISGhiIkJESn/MiRI0hOTpaeOzk54eDBgyhTpkyaOt7e3llzMHkIe+yIiIgoXUuWLEFERATs7OwwbNgwbN26Fba2tti4cWOanrugoCA0btwY9vb2OuV2dnZSb11qj12+fPlgamqqs+2PP/4IIyP2N30pmRBC6LsR9PliY2NhbW2NmDFWUJnK/nsDIiKiCTH6bgFlgvS3PiYmzYSWDzEaExERGYC4pEz008TFZbiqpaXlZ7SG9IXBjoiIyAAoA99kvHLgx2e2fogDe7kLr7EjIiIiMhDssSMiIjIAan+rjFceF5l1DSG9YrAjIiIyAJYmmZhAx+vmDBaHYomIiIgMBIMdERERkYFgsCMiIiIyEAx2RERERAaCwY6IiIjIQDDYERERERkIBjsiIiIiA8FgR0RERGQgGOyIiIiIDASDHREREZGBYLDLZkIIfTeBiIiIDBSDXTbSarWQyd7dy+/Vq1d6bg0REREZGga7bCKEgFz+7uMePHgwWrRogfj4eD23ioiIiAwJg102EEJIPXWhoaG4evUqZsyYAXNzcz23jIiIiAyJkb4bkBekhrqtW7di/fr1yJcvH2rUqIGUlBQYGWXuFCQmJiIxMVF6Hhsb+1XbSkRERLkXg102SUhIwPHjx3HlyhXY2dlBLpdDLpdDo9FAoVBkeD+BgYGYOHFimvJyCasgFxZfs8lERGSoxuz74l08mN70KzSEvjYOxWYRrVar89zMzAxTp05F165dERUVhZEjR0Kr1UKhUKSp+yn+/v6IiYmRHo8fP/7aTSciIqJcij12WUCr1UoTJS5cuICkpCSYmZmhYsWKGDNmDFJSUnD06FFMmDABkyZNylTPnampKUxNTbP6EIiIiCgXYrD7yt6f/Tp27Fhs27YN5ubmePjwITp27Ijx48djzJgx0Gg0OHz4MORyOSZMmJCp4VgiIiKi9HAo9itLnSgxd+5crFq1CkFBQbh69Sr69++PtWvX4tGjR7C2tsbYsWPh6emJ9evXY+XKlXpuNRERERkC9thlkb///hujRo1CzZo1ERwcjOXLl+OXX35BzZo1ER8fDxsbG4wcORIuLi7o1q2bvptLREREBoA9dl9ZcnIykpKSEBYWBjc3N5w7dw7dunVDYGAg+vXrh6SkJMyZMwfHjx+HnZ0dBgwYAIVCAY1Go++mExERZYkXL16gadOmsLS0RMmSJXHkyJF065UtWxZKpVJ6KBQKDBo0CACwceNGndfMzc0hl8vx77//AgAeP36MZs2awcbGBoULF8a2bduy7fhyEga7L/T3339LtwebNm0aTp06BRMTEzRp0gQDBgyAp6cnFi9ejL59+wIA1Go1jhw5gkuXLunsh9fYERGRoRowYAAcHR3x4sULzJo1C23btsXLly/T1AsLC4NarYZarUZ0dDRUKhVatWoFAOjYsaP0mlqtxoQJE1C3bl3Y29sDADp16oRSpUrhxYsX2LFjBwYMGICbN29m63HmBByK/QK3bt1Cv379ULlyZSgUCixbtgxXrlwBAHh7e+P06dNQqVRo1KgRgHf/YvH19UViYqL0LxAiIiJDplarsWvXLty7dw8WFhZo0aIF3NzcsHv37k9eirR3716oVCp4enqm+3pQUBD8/Pyk9zh58iT27t0LY2NjVKxYET/88AM2bNiAKVOmZMlx5VQMdl+gZMmS6N69OyZPnozY2FgcO3YM5cqVAwDUrVsXXbt2xZo1a1CmTBkUK1YMKSkpkMvlOH36tDT8yp46IiIyZOHh4VAqlXB2dpbK3NzcEBYW9sntgoKC0KlTJ2lS4vsuXryIu3fvok2bNgDerUiR+kglhPjP9zBEHIr9DFqtVvrlKVy4MIyNjVG0aFFs3boVz58/l+p169YNK1aswIwZM9CqVSuMGDECZ8+ehbGxMVJSUhjqiIjI4KnVaqhUKp0ylUoFtVr90W2io6Nx4MABdO7cOd3Xg4KC4OPjI+3XysoKtWvXxoQJE5CYmIh//vkHwcHBiIuL+3oHkkuwx+4zpK5Td+vWLXh7e+P8+fPYtm0btmzZgrFjxyIwMBD58uUD8K5Xr2TJkjrbazSaTN8jloiIKDdSKpVp7mseGxsLpVL50W22bNkCd3d3lCpVKs1rGo0GmzdvxqpVq3TKN27ciAEDBsDZ2RlFihRBly5dPhkeDRV77D7T4cOHUb58eaxbtw6Ojo4YNGgQWrZsievXr+Onn36SZun07t0bf/31l8627KkjIqK8onjx4lCr1Xj69KlUdu3aNZQtW/aj2wQFBX20t+7w4cMQQuD777/XKXd1dUVISAhevHiBc+fO4d9//0W1atW+zkHkIgx2n8nFxQW9evVCQEAA1q5dC5lMhhEjRqBVq1a4evUqmjRpgu+++w579uxB9erV9d1cIiIivVAqlfDx8UFAQADi4+MREhKCK1euwMfHJ9364eHhCA0NRfv27dN9PSgoCO3bt08z8nX9+nWo1WokJiYiKChIWm4sr+F4YAYIIdJcvFmqVCkMHToUCoUC48ePBwB07doVw4cPR4ECBfDPP/9ArVbj0KFDMDIy4kQJIiLKs5YsWQJfX1/Y2dnB2dkZW7duha2tLTZu3Ihp06bpTHIICgpC48aNpWVM3pc6w/bkyZNpXtu/fz8CAwORmJiImjVrYv/+/Xny3uoy8f4UEvqkFStWwNHREc2bN5fK7ty5g4ULF2L79u345Zdf8OOPP6bZLiUlJcuuqYuNjYW1tTVchm6D3NQiS96DiIjoQw+mN9V3E/KM1L/1MTExaSaifIg9dhkUGRmJvXv34vbt2zAzM0PDhg0BAMWKFUO/fv1w8uRJDBw4EHFxcejRo4fOtpwoQUREhkCblCD9/P6MU0tLS300h9LBa+w+QqvV6jx3cnLC6NGjUbVqVfj5+eHgwYPSa6VKlUKZMmWQP39+7N+/H+wEJSIiQ/R4bmvp8f7tvSjnYFdSOrRarbSkSWRkJJKTk1GwYEHUrl0bSqUS06dPx8iRI2FkZIQGDRogLi4OMpkMAQEBaNmyJWQyWbrX5RERERFlJV5j9wnjx4/H1q1b8ebNGxQpUgQjRoxAixYtcOvWLUyfPh27d+/G999/j4cPH0IIgXPnzkGhUOgEw6zGa+yIiCi7vD8Ue2NyY+lnDsVmrcxcY8eh2Pe8P/y6evVqLF++HOPHj8fatWthZ2eHiRMnYsmSJShTpgwCAwMxc+ZMAECdOnVw5syZbA91RERE2UluYiY9LC0tpQflHOyxS0dISAgiIiKgUCh0JkL0798fR48eRVBQEKpWrQpAd8ZrVs5+/Rj22BERkT5wVmz2YY/dF7h37x5++OEH9O3bF8+ePQPwLrAB79bhMTU1xeLFi6X6qUFOCMHZr0RERKRXeT7Yfdhh6eLign379qFo0aI4evSo1AuXOkxbq1YtJCYmptkPJ0oQERGRvuXpYKfVaqVAlpiYCI1GA2NjYzRo0ACLFi3CxYsX0b59e8TExCA5ORkpKSm4cOECrKys9NxyIiIiorTy9Nhh6iSHadOm4Z9//kFsbCwCAwNRtWpVNGrUCBs3bkSnTp1Qo0YNFC1aFJaWllCr1TpDsUREREQ5RZ7usQOA+fPnY+7cuShRogRSUlJQv359rF+/HklJSWjcuDE2btwIjUaDsLAwBAQE4Pr16zA2NpauuyMiIiLKKfJcsPvwjhLJyclYsWIFpk+fjuPHj6NHjx7o1asXNm3ahKSkJDRq1AgLFy7E69evMWXKFGk7hUKR3U0nIiIi+qQ8NRQrhJCGX/fv34/o6GicO3cOZcuWlerMnz8fMpkM/fr1g0wmw48//ohGjRph8+bN8PX1RbNmzRASEsLJEkRERJTj5Jlg9/4tvkaPHo2FCxeiUKFCuHnzJr799ltUrlwZDg4OAIB58+ZBLpejW7duyJcvH5o0aYJGjRph5cqVGDJkCJ4+fYoCBQro83CIiIiI0sgTwe79UHf27FlcvXoVhw4dgru7O6ZNm4agoCAUKVIEnTt3Rr58+QAAv/zyC1xdXfH9998DeLecSbNmzfDdd99xlW0iIiLKkfJEsEsNdRs2bMCuXbugUqlQu3ZtyGQyTJs2DVqtFvPnz4cQAr6+vrC3twcADBkyBMD/3VFCJpMx1BEREVGOZdDB7tixY4iIiEDHjh0BAGFhYTh58iRUKhUiIyORP39+AMD06dMhk8mwZMkSvHnzBn5+frC2tpb2wztKEBERUW5gsInl9evXWLBgAR4/fgwjIyO0a9cOgYGBcHBwwNKlSzFz5kwMHz4cLi4uAIDAwEDExsbi6tWr/3kfNiIiIqKcyGCDnY2NDcaPH4+5c+di0aJFSE5ORqdOneDn54e3b9/it99+g5GREYYOHQpnZ2cAwOLFi6Xr8d6/Lo+IiIgoNzDodewqV66M4cOHo0CBAli+fDk2bNgAABg3bhxatWqFY8eOYcGCBXj48KG0DUMdERER5VYG22OXqmLFihg9ejRmzJiB5cuXAwA6deqEcePGQS6XY/HixXB1dcWAAQOkbXJjqLs2sRGHkImIiPI4mRBC6LsR2eHixYuYMWMGnj59ir59+0oTKtatW4dOnTrl2jtJxMbGwtraGjExMQx2REREBigzf+sNeij2fak9d87OzlixYgV+/fVXAICvry8UCgU0Go2eW0hERET0ZXJ9sPvw3q+fkhruTE1NcenSJZ3XcmuPHREREVGqXH2NnVarle79euTIETx//hy1atWCk5MTTExM0t3G3d0dixYtQtGiRbOzqURERERZLlcHu9RQN3LkSKxevRqmpqbQaDQYPXo0OnToAEdHx3S3K168OADdYEhERESU2+XKVPP+fI+TJ0/i9OnT2L17N27duoXu3btjxYoVWL58OaKioj65H4Y6IiIiMiS5sscudTmSNWvWIDQ0FJUrV0adOnUAvLuDhImJCbZs2QKZTIbevXt/tOeOiIiIyJDkymCX6uDBg9i2bRvq1KkDtVoNpVIJAJg4cSIAYPv27YiNjYW/vz/s7Oz02VQiIiKiLJdrxiLTW25vy5YtGDJkCMLDw7Fu3TrExMRIr02cOBFeXl6IjIyEra1tdjaViIiISC9yRY/d+5McEhISkJKSIvXOzZ07F7GxsZg/fz6MjY3x448/Sov3zZ07l/d+JSIiojwjx/fYvR/qZsyYgbZt26JcuXKYPXs2QkNDAQCrVq1CnTp1MGfOHGzdulWn546hjoiIiPKKHBvsUhceTg1148aNwy+//AIvLy8MHz4cS5cuxezZs3HixAkAwOrVq+Hh4YERI0ZIZakY6oiIiCgvyLFDse8vRbJ7925s374de/fuRbVq1XDu3Dk8ePAAMpkM8fHxMDY2Rq1atbBy5UoUKVIETZs21WPLiYiIiPQjxwW7tm3b4ttvv8XChQsBvJs0YW9vjz59+qBatWoICQlB586dsWbNGnz77bfw8fGBsbEx3rx5g0aNGmHs2LEAAI1Gw9uEERERUZ6So4JdYmIi+vXrh9q1a0tlMpkMpUuXRokSJfD69WvMmjULY8aMQZcuXQAAxYoVw4kTJ1CmTBk0atRI2o6hjoiIiPKaHHON3ZUrV2BqagpPT0+YmJhg8eLF0qLDtra2yJcvH+Li4hAVFQVXV1cAwPPnz1G9enUsXLgQP//8sz6bT0RERKR3OSLYLVmyBO7u7jh27BjkcjmSk5ORL18+PHnyBC1atJDqxcbGwsLCAidPnsSGDRvQrVs33Lt3D23atIFcLodGo9HjURARERHpV44Idm3atEGPHj3QrFkzHDlyBMbGxmjevDkWLFiA69evS5MhSpcujX79+uH06dOYOnUq4uPjcejQIWlJEw6/EhERUV4mE+nd0iEbpa4xp1arMXbsWKxcuRKHDh1CnTp1EB8fj4MHD2LEiBEoXrw4Dhw4AAB48OABTE1N8e2330IulyMlJQVGRjnqcsFsExsbC2tra8TExEgLMxMREZHhyMzfer322Gm1WmmNuc2bN8PR0REJCQlo2rQpjh07BnNzczRq1AizZ8/G3bt30axZMwBAoUKF4OTkBLlcDq1Wm2dDHREREdH79BrsUteq8/f3x4QJE5AvXz78/PPPqFKlCpo0aYIjR47ohLsTJ05g5MiR6e6DiIiIKK/Te1dXREQEdu3ahVmzZqFDhw4AgEePHmH8+PFo3rw5fv/9d9StWxdeXl7Yv38/atWqpecWExEREeVMeu/uSkxMxKNHj3TGjF1cXDBu3Dg4OTmhZcuWOHjwICwsLODh4QGFQsHZr0RERETpyNZgl948jcKFC6Nu3boICgrCy5cvAbxblLhEiRJwc3ODiYkJAgMDdbbh7FciIiKitLIt2L0/UeLRo0cICwvDq1evAAA//vgj7t27h7lz5+Lt27cAIP13w4YNOHbsWHY1k4iIiCjXypZr7IQQ0iSH8ePH4/Dhw7hx4wY8PT3h7u6OyZMn48GDB9i3bx9CQkLg4eGBM2fOQKPRwNPTEzKZDFqtlhMliIiIiD4hW5JSak/dtGnTsHz5ckydOhW3b9+Gubk55s+fj5s3byIgIAATJ06Eh4cHHj58iEqVKuHcuXPSNXUMdURERESfluU9dqlDsK9evcKxY8ewZMkSeHl54fDhw9i/fz/mz5+PUqVKAQC8vb3h7e2N5ORkGBsbA0CeXnyYiIiIKDOyrBvsypUriI6Ohlwuh0wmg5mZGWJiYlC6dGns3bsXLVu2xOzZs9GjRw8kJiZi5cqVOH36NABIoQ4AQx0RERFRBmVJsNu1axdq1KiBgIAAvHjxAgCQlJQEmUyGn3/+GV27dsXMmTPRt29fAO8mUwQHByMqKiormkNERESUJ3z1YJeYmIiQkBAkJCTg3r17mDJlCp49ewYbGxsEBATg4MGDqFOnDvr16wchBGJjYzF06FAkJSXBx8fnazeHiIiIKM+QifQWl/tC586dQ7NmzVCjRg3Ex8ejXLly8Pf3x7fffouFCxdiyJAh+P777yGEQGJiIl69eoV//vkHxsbG0Gg0XKcuEzJzY2AiIiLKfTLzt/6rXsCm1WohhEC1atXQpUsX2NnZQavVYufOnZDJZBg/fjwGDRoEd3d37NixA0IIFClSBP3794eRkREnShARERF9ga+Som7cuAGVSoUCBQpIZc7Ozti4cSNOnz4NS0tLbNq0CVOmTMGYMWPg4eGBmjVr6oQ4jUbDUEdERET0Bb74GrvffvsNFSpUQJ06dbB582b8888/AAA/Pz+oVCrMnTsXfn5+aNGiBc6ePYuZM2ciKioqTYjj8CsRERHRl/miLrKkpCQcOXIEDg4OUCgUWLZsGVQqFb755htMmTIFXl5euH//PgDgp59+glwux+rVq+Hq6orBgwd/lQMgIiIione+ePJEVFQUAgMD8ejRIzg5OaF79+4YPnw47O3tce/ePVy+fBnBwcFo2bIlAGDt2rXo3Lkze+i+Ek6eICIiMmyZ+Vv/xUOxjo6OGD16NAoUKICLFy/iwoULOHHiBIYPHw5vb2+4uLhId5YAgK5du0q3CSMiIiKir+erLXcSGRmJadOm4cyZM+jYsSP8/PwAAC9fvoStrS20Wi3v95oF2GNHRERk2LK1xy6Vk5MTxo0bh5o1a2LLli0IDAwEANja2kKj0TDUEREREWWxr5q2HB0dMW7cOFSvXh0hISH46aefAHDGKxEREVF2+OrdaI6Ojhg7diyKFi2KZ8+eIQtubEFERERE6ciSFYEdHR0xb9482NjYQCaTQQgBmUyWFW9FRERERP9flt3qwdbWFgA4aYKIiIgom2R54mKoIyIiIsoeTF1EREREBoLBjoiIiMhAMNgRERERGQgGOyIiIiIDwWBHREREZCAY7IiIiIgMBIMdERERkYFgsCMiIiIyEFl25wnKHqn34o2NjdVzS4iIiCgrpP6NT/2b/ykMdrlcdHQ0AMDFxUXPLSEiIqKs9ObNG1hbW3+yDoNdLpd6T95Hjx7958km/YmNjYWLiwseP34MlUql7+bQJ/Bc5R48V7kDz9OXE0LgzZs3yJ8//3/WZbDL5VLvxWttbc0vTC6gUql4nnIJnqvcg+cqd+B5+jIZ7bzh5AkiIiIiA8FgR0RERGQgGOxyOVNTUwQEBMDU1FTfTaFP4HnKPXiucg+eq9yB5yl7yURG5s4SERERUY7HHjsiIiIiA8FgR0RERGQgGOyIiIiIDASDXS6wePFiFCpUCGZmZqhevTrOnz//yfrbt29HqVKlYGZmBjc3N+zfvz+bWpq3ZeY8rVixAh4eHvjmm2/wzTffwMvL6z/PK309mf1OpdqyZQtkMhl++OGHrG0gSTJ7rl6/fo0BAwbAyckJpqamKFGiBP8fmA0ye57mzZuHkiVLwtzcHC4uLvDz80NCQkI2tdbACcrRtmzZIkxMTMTq1atFWFiY6NWrl7CxsRHPnj1Lt/5ff/0lFAqFmDlzprh+/boYP368MDY2FlevXs3mluctmT1PHTp0EIsXLxYXL14UN27cEF27dhXW1tbiyZMn2dzyvCez5yrV/fv3RYECBYSHh4fw8fHJnsbmcZk9V4mJiaJKlSqiSZMm4tSpU+L+/fvi+PHj4tKlS9nc8rwls+dp48aNwtTUVGzcuFHcv39fHDx4UDg5OQk/P79sbrlhYrDL4apVqyYGDBggPddoNCJ//vwiMDAw3fpt27YVTZs21SmrXr266NOnT5a2M6/L7Hn6UEpKirCyshLr1q3LqibS//c55yolJUXUqlVLrFy5Uvj6+jLYZZPMnqulS5eKIkWKiKSkpOxqIonMn6cBAwaI7777Tqds2LBhonbt2lnazryCQ7E5WFJSEi5cuAAvLy+pTC6Xw8vLC2fOnEl3mzNnzujUB4BGjRp9tD59uc85Tx96+/YtkpOTpXv/Utb43HM1adIkODg4oEePHtnRTMLnnas9e/agZs2aGDBgAL799luUK1cO06ZNg0ajya5m5zmfc55q1aqFCxcuSMO19+7dw/79+9GkSZNsabOh471ic7B///0XGo0G3377rU75t99+i5s3b6a7TVRUVLr1o6Kisqyded3nnKcPjR49Gvnz508Tyunr+pxzderUKaxatQqXLl3KhhZSqs85V/fu3cPRo0fRsWNH7N+/H3fu3EH//v2RnJyMgICA7Gh2nvM556lDhw74999/UadOHQghkJKSgr59+2Ls2LHZ0WSDxx47Ij2bPn06tmzZgp07d8LMzEzfzaH3vHnzBp07d8aKFStgb2+v7+bQf9BqtXBwcMCvv/6KypUro127dhg3bhyWLVum76bRe44fP45p06ZhyZIlCA0NxY4dO7Bv3z5MnjxZ300zCOyxy8Hs7e2hUCjw7NkznfJnz57B0dEx3W0cHR0zVZ++3Oecp1SzZ8/G9OnT8ccff6B8+fJZ2UxC5s/V3bt38eDBAzRv3lwq02q1AAAjIyPcunULRYsWzdpG51Gf871ycnKCsbExFAqFVFa6dGlERUUhKSkJJiYmWdrmvOhzztNPP/2Ezp07o2fPngAANzc3xMXFoXfv3hg3bhzkcvY5fQl+ejmYiYkJKleujCNHjkhlWq0WR44cQc2aNdPdpmbNmjr1AeDw4cMfrU9f7nPOEwDMnDkTkydPxu+//44qVapkR1PzvMyeq1KlSuHq1au4dOmS9GjRogXq16+PS5cuwcXFJTubn6d8zveqdu3auHPnjhS+AeD27dtwcnJiqMsin3Oe3r59mya8pYZxwbucfjl9z96gT9uyZYswNTUVa9euFdevXxe9e/cWNjY2IioqSgghROfOncWYMWOk+n/99ZcwMjISs2fPFjdu3BABAQFc7iQbZPY8TZ8+XZiYmIjg4GARGRkpPd68eaOvQ8gzMnuuPsRZsdkns+fq0aNHwsrKSgwcOFDcunVLhISECAcHBzFlyhR9HUKekNnzFBAQIKysrMTmzZvFvXv3xKFDh0TRokVF27Zt9XUIBoXBLhdYuHChKFiwoDAxMRHVqlUTZ8+elV7z9PQUvr6+OvW3bdsmSpQoIUxMTETZsmXFvn37srnFeVNmzpOrq6sAkOYREBCQ/Q3PgzL7nXofg132yuy5On36tKhevbowNTUVRYoUEVOnThUpKSnZ3Oq8JzPnKTk5WUyYMEEULVpUmJmZCRcXF9G/f3/x6tWr7G+4AZIJwX5PIiIiIkPAa+yIiIiIDASDHREREZGBYLAjIiIiMhAMdkREREQGgsGOiIiIyEAw2BEREREZCAY7IiIiIgPBYEdERERkIBjsiIiIiAwEgx0RGbyuXbtCJpNBJpPB2NgYhQsXxqhRo5CQkJCmbkhICDw9PWFlZQULCwtUrVoVa9euTXe/v/32G+rVqwdra2solUqUL18ekyZNwsuXL/+zTX369IFCocD27dvTbe8PP/yQpvz48eOQyWR4/fq1VJaUlISZM2eiQoUKsLCwgL29PWrXro01a9YgOTn5o++/YsUKVKhQAUqlEjY2NqhYsSICAwP/s91ElLMx2BFRntC4cWNERkbi3r17mDt3LpYvX46AgACdOgsXLoSPjw9q166Nc+fO4cqVK/jxxx/Rt29fjBgxQqfuuHHj0K5dO1StWhUHDhzAtWvXMGfOHFy+fBlBQUGfbMvbt2+xZcsWjBo1CqtXr/7sY0pKSkKjRo0wffp09O7dG6dPn8b58+cxYMAALFy4EGFhYelut3r1agwdOhSDBw/GpUuX8Ndff2HUqFFQq9Wf3ZaMtJWIsoG+b1ZLRJTVfH19hY+Pj05Zy5YtRcWKFaXnjx49EsbGxmLYsGFptl+wYIEAIN3Y/Ny5cwKAmDdvXrrv9183M1+7dq2oUaOGeP36tbCwsBCPHj36z/YKIcSxY8cEAGn/M2bMEHK5XISGhqapm5SUJNRqdbrv7+PjI7p27frJNgohxKpVq0SZMmWEiYmJcHR0FAMGDJBee/jwoWjRooWwtLQUVlZWok2bNiIqKkp6PSAgQFSoUEGsWLFCFCpUSMhkMiHEu8+mR48ewt7eXlhZWYn69euLS5cu/WdbiChj2GNHRHnOtWvXcPr0aZiYmEhlwcHBSE5OTtMzB7wbNlUqldi8eTMAYOPGjVAqlejfv3+6+7exsfnk+69atQqdOnWCtbU1vL29PzrU+182btwILy8vVKxYMc1rxsbGsLS0THc7R0dHnD17Fg8fPvzovpcuXYoBAwagd+/euHr1Kvbs2YNixYoBALRaLXx8fPDy5UucOHEChw8fxr1799CuXTudfdy5cwe//fYbduzYgUuXLgEA2rRpg+fPn+PAgQO4cOECKlWqhAYNGmRo+JqIMkDfyZKIKKv5+voKhUIhLC0thampqQAg5HK5CA4Olur07dtXWFtbf3Qf5cuXF97e3kIIIby9vUX58uU/qy23b98WxsbG4sWLF0IIIXbu3CkKFy4stFqtTnsz0mNnbm4uBg8enOk2REREiBo1aggAokSJEsLX11ds3bpVaDQaqU7+/PnFuHHj0t3+0KFDQqFQ6PQ0hoWFCQDi/PnzQoh3PXbGxsbi+fPnUp2TJ08KlUolEhISdPZXtGhRsXz58kwfBxGlxR47IsoT6tevj0uXLuHcuXPw9fVFt27d0KpVq8/alxDis9uxevVqNGrUCPb29gCAJk2aICYmBkePHs22djg5OeHMmTO4evUqhgwZgpSUFPj6+qJx48bQarV4/vw5IiIi0KBBg3S3v3HjBlxcXODi4iKVlSlTBjY2Nrhx44ZU5urqinz58knPL1++DLVaDTs7OyiVSulx//593L1797OOhYh0Gem7AURE2cHS0lIaSly9ejUqVKiAVatWoUePHgCAEiVKICYmBhEREcifP7/OtklJSbh79y7q168v1T116hSSk5NhbGyc4TZoNBqsW7cOUVFRMDIy0ilfvXq1FKRUKlW6w6SvX7+GQqGQhlhLlCiBmzdvZuJT0FWuXDmUK1cO/fv3R9++feHh4YETJ06gSpUqn73P9304FKxWq+Hk5ITjx4+nqftfw9dElDHssSOiPEcul2Ps2LEYP3484uPjAQCtWrWCsbEx5syZk6b+smXLEBcXh/bt2wMAOnToALVajSVLlqS7//eXI3nf/v378ebNG1y8eBGXLl2SHps3b8aOHTuk7UqWLImwsDAkJibqbB8aGorChQtLYbJDhw74448/cPHixTTvlZycjLi4uAx9HsC7HjcAiIuLg5WVFQoVKoQjR46kW7d06dJ4/PgxHj9+LJVdv34dr1+/lvaTnkqVKkmhtlixYjqP1B5MIvpC+h4LJiLKaulds5acnCwKFCggZs2aJZXNnTtXyOVyMXbsWHHjxg1x584dMWfOHGFqaiqGDx+us/2oUaOEQqEQI0eOFKdPnxYPHjwQf/zxh2jduvVHZ8v6+PiIdu3apSnXaDTC0dFRLFq0SAjxbuaog4ODaNu2rfjnn39EeHi4WLVqlbCyshJLly6VtktISBAeHh7im2++EYsWLRKXLl0Sd+/eFVu3bhWVKlUSFy9eTLcdffv2FZMmTRKnTp0SDx48EGfOnBFNmzYV+fLlE//++68Q4t3MXTMzMzF//nxx+/ZtceHCBbFgwQIhhBBarVa4u7sLDw8PceHCBXHu3DlRuXJl4enpKb1H6qzY92m1WlGnTh1RoUIFcfDgQXH//n3x119/ibFjx4q///473bYSUeYw2BGRwfvYZITAwECRL18+nWVBdu/eLTw8PISlpaUwMzMTlStXFqtXr053v1u3bhV169YVVlZWwtLSUpQvX15MmjQp3eVOoqKihJGRkdi2bVu6++rXr5/O8iu3bt0S//vf/0T+/PmFpaWltHTI+5MshHgX7gIDA4Wbm5swMzMTtra2onbt2mLt2rUiOTk53fcKDg4WTZo0EU5OTsLExETkz59ftGrVSly5ckWn3rJly0TJkiWFsbGxcHJyEoMGDZJey+hyJx+KjY0VgwYNEvnz5xfGxsbCxcVFdOzYMc2SL0T0eWRCfMFVwERERESUY/AaOyIiIiIDwWBHREREZCAY7IiIiIgMBIMdERERkYFgsCMiIiIyEAx2RERERAaCwY6IiIjIQDDYERERERkIBjsiIiIiA8FgR0RERGQgGOyIiIiIDASDHREREZGB+H8Kh1W1xS1/1QAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "clf = LogisticRegression()\n", - "cmp = ImputationSelector(clf=clf, strategies=strategies, cv=5)\n", - "cmp.fit_compute(X_missing, y)\n", - "result_plot = cmp.plot()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can also pass a sklearn pipeline instead of a classifier." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from sklearn.pipeline import Pipeline\n", - "from sklearn.preprocessing import StandardScaler\n", - "\n", - "steps = [(\"scaler\", StandardScaler()), (\"LR\", LogisticRegression())]\n", - "clf = Pipeline(steps)\n", - "cmp = ImputationSelector(clf=clf, strategies=strategies, cv=5, model_na_support=False)\n", - "cmp.fit_compute(X_missing, y)\n", - "result_plot = cmp.plot()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "jp-MarkdownHeadingCollapsed": true - }, - "source": [ - "## Scikit Learn Compatible Imputers. \n", - "\n", - "You can also use any other scikit-learn compatible imputer as an imputing strategy.\n", - "e.g. [feature engine](https://feature-engine.readthedocs.io/en/latest/index.html) library provides a host of other imputing stratgies as well. You can pass them for comparision as well." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.10" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/tutorials/nb_metric_volatility.ipynb b/docs/tutorials/nb_metric_volatility.ipynb deleted file mode 100644 index 24b036b8..00000000 --- a/docs/tutorials/nb_metric_volatility.ipynb +++ /dev/null @@ -1,342 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Metric Volatility Estimation\n", - "\n", - "[![open in colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ing-bank/probatus/blob/master/docs/tutorials/nb_metric_volatility.ipynb)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The estimation of AUC of your model could be influenced by, for instance, how you split your data. If another random seed was used, your AUC could be 3% lower. In order to understand how stable your model evaluation is, and what performance you can expect on average from your model, you can use the `metric_volatility` module.\n", - "\n", - "### Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture\n", - "!pip install probatus" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.datasets import make_classification\n", - "from sklearn.ensemble import RandomForestClassifier\n", - "\n", - "from probatus.metric_volatility import BootstrappedVolatility, SplitSeedVolatility, TrainTestVolatility\n", - "\n", - "X, y = make_classification(n_samples=1000, n_features=10, random_state=1)\n", - "clf = RandomForestClassifier(n_estimators=2, max_depth=2, random_state=0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### TrainTestVolatility\n", - "The class that provides a wide functionality for experimentation with metric volatility is TrainTestVolatility. Please refer to the API reference for full description of available parameters.\n", - "\n", - "By default, the class performs a simple experiment, in which it computes the metrics on data split into train and test set with a different random seed at each iteration. Having computed the mean and standard deviation of the metrics, you can analyse the impact of random seed setting on your results and get a better estimation of performance on this dataset.\n", - "\n", - "When you run the `fit()` and `compute()` or `fit_compute()`, the experiment described above is performed and the report is returned. The `train_mean` and and `test_mean` show an averaged performance of the model, and `delta_mean` indicates on average how much the model overfits on the data. \n", - "\n", - "By looking at `train_std`, `test_std`, `delta_std`, you can assess the stability of these scores overall. High volatility on some of the splits may indicate the need to change the sizes of these splits or make changes to the model." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
train_meantrain_stdtest_meantest_stddelta_meandelta_std
roc_auc0.8318180.0364070.8165380.0437320.015280.027516
\n", - "
" - ], - "text/plain": [ - " train_mean train_std test_mean test_std delta_mean delta_std\n", - "roc_auc 0.831818 0.036407 0.816538 0.043732 0.01528 0.027516" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Basic functionality\n", - "volatility = TrainTestVolatility(clf, iterations=50)\n", - "volatility.fit_compute(X, y)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The results above show quite unstable results, due to high `train_std` and `test_std`. However, the `delta_mean` is relatively, which indicates that the model might underfit and increasing the complexity of the model could bring improvements to the results.\n", - "\n", - "One can also present the distributions of train, test and deltas for each metric. The plots allows for a sensitivity analysis." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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ZMgQnn/+eSQxevRorrzySn7729/WufyUKVMKbrtQLq7MSqRP3y0ZtHP1mtcPPnAvf7ztVlauWsm8uXN54z9T2Wbb7dZap2PHjgwb/jUABuw4iJfHj8vb9qGHHkqHDsl462eeeYZzzjkHgH79+rHZZpsxffp0Hn/8cc4444w1h927detWZ6xPPPEEv/nNb1i2bBnvv/8+O++8c73FVWPabiH2BWZGxAIASX8C9gDWKq4iYhQwCqC6ujpKHaRZsQXJn/WYJx9n6pRXefgvDwKweNEiZr45nS23+nKd6y5atIgRI0Ywbdo0JLFixYq8y+26665rrh/1zDPP8M1vfpNOnToB8K1vfYunn34aSRx++OH06NEDqD/HzJkzhyOOOIK5c+fy6aefNnhtqieffLLgtgvl4sqsRDZIkwXAjDenM/r6/+GRJ59mo65d+eHJJ7B8+fJ11mmXMwC+bZu2rFyZ/1tsp5y2a07h1RYRBf2c+OOPP+a0005j4sSJbL755lx00UVrru9SVVXF6tWrAda65kuhbbcgs4HdJG0AfAIMA8aXNySz0npr5kzatmlLj56bQASXXXk1Q/cdvtYys996q871f/aznzF06FAefPBBZs2axd577513uSzzF8Dpp5/OWWedxSGHHMKYMWO45JJLgLXzF3yWw4qRvzzmyqwMlixezIYbdqZzly7Me28uTz3xeGZt77nnntx5550ATJ06lblz57LVVlux3377cf3117Nq1SqAOg99f/LJJ7Rp04YePXqwZMmStQ7j9+nThwkTJgBw//33r5leaNstRUSMA+4HJgKvkOTKUWUNyqyE3n9/AT8983ROGHkqkth72HBuvWnUmqNPb06fxkcffbTWOhtuuCFLly5d83rRokVsvvnmANxyyy0F9bvnnnvy5z//mY8//piPPvqIBx98kK9+9asMGzaMe++9l4ULFwL155jcfm+99dY10/v06cPEiRMBmDhxIjNnzgRoVNuF8pErW68V8vPjQjRmrFUhBuw4iK233Za9dtuZLfr0Zdfddsus7dNPP51TTjmFHXbYgXbt2nHbbbfRvn17TjnlFKZNm8aAAQOoqqri+9//Pqeeeuo663fv3p0RI0bQv39/tthiCwYPHrxm3jnnnMMRRxzBzTffzNChQ9dML7TtliQiLgYuLncc1jrV5K6sc099ln3yCcOGDF5zKYbDjziKU0/7MQDHjDiBt2e/xfA9dyci6N6jB7fcee9a63/lq3vx39dcxbAhg/nZhRfw05/+lBEjRnD11Vezzz77FBTDTjvtxPHHH8+uu+4KwEknncSgQYMAuPDCC9lrr71o27YtgwYNqrNgu+SSS/j2t7/N5ptvzm677bamiDrssMO47bbb2HHHHdlll13YeutkH/fr16/gtgulug7BlUN1dXWMH+8j79Z0U6dOZbvttmt4wUYqZYKrz6ZdWu51LPO9N5ImRER1Hau0KM5f1hx15a5KyT2N1ZJzVV0ak8N8WtDMzMwsQz4taNaKHXLIIcyePXutaVdddRX77rtvmSIyMyvMZZddxn333bfWtG9/+9tceOGFZYroMy6uzFqxhx56qNwhmJk1yYUXXlgRhVQ+Pi1oZmZWASppDLStrbHvjYsrMzOzMuvQoQMLFy50gVWBIoKFCxeuuVBzIXxa0MzMrMx69erFnDlzWLBgwVrTF3+S/6rmle6Dju3KHUKmOnToQK9evQpe3sWVmZlZmbVr1y7vbVqueeyNMkTTfFldY7ClcnFl67enLs+kmU7pzVM/2uOcepf74IOFfPuQAwGYP28ebdu2oXuPngA88uTTtM+5nU197rr9Vvbd72tssunnmxG1mZmVg4srswx169adJ55Jbq78m8t/RadOnfjBj85sdDt333ErAwbu2GBxtXLlSqqq/DE2M6skHtBuViL33HUH+w8dwrAhgzn3rB+zevVqVq5cyWkjv8feu1ez1247M/qG/+HPD9zHq6+8zCknHMewIYP59NNP12pnyJAhXHjhhey5555cd911zJw5k6FDhzJgwACGDx/OnDlzAHjvvfc49NBDGTBgAAMHDmTcuHF1xnbwwQez8847069fP0aPHg0khVvXrl3XLHP33Xdz0kknNbptM7PWxl95zUpg6mtTeOThv/DwY2Ooqqri7B/9kD8/cC99+m7JwoULGfNcctuURR9+yEZdu3LTqOv5r99cQ/8BA/O2t3jxYsaOHQvAAQccwEknncQxxxzDqFGjOOOMM7j//vv54Q9/yPDhwznttNNYuXIlH3/8cZ3x3XrrrXTr1o2PP/6Y6upqDjvsMDp37lzn8o1p28ystfGRK7MSeHrMk0yaOIGv7f0Vhg0ZzHPPPs2smTPps+WXeHPaG1x07k946vHH6LLRRgW1d+SRR655Pm7cuDWvv/vd7/L0008DMGbMGE455RQAqqqq6NKlS53tXXPNNQwcOJDdd9+dOXPm8Oabb9bbf2PaNjNrbXzkyqwEIoKjjh3BuRddvM68p/79Ik889g9G3/gH/vbQn7nq9//TYHudOnUqqF9JDS7z+OOPM3bsWJ5//nk6duzIkCFDWLZsGW3atFnrmjvLlq19A9lC2jYza4185MqsBPbcex8eevABFi58H0h+VTjn7dm8//4CIoJDvnkY55x/ES9PngRApw07s3TpkoLa3m233bj33nsBuOOOO9hzzz0BGDp0KDfccAMAq1atYvHixXnXX7RoEd26daNjx45MmTKFF198EYA2bdqw8cYbM23aNFavXs2DDz64Zp1C2zYza4185MrWb0PPz6SZjxYva3ihemzXrz8/Oe8CvnPo11m9ejXtqtpxxTW/p23btpx12vcJAiEuuvRXABx5zHH85PQf0KFjxwYv4XDddddx4okncvnll7Ppppty8803r5l+8sknc+ONN1JVVcWNN97Irrvuus76X//61xk1ahQDBw5k2223ZfDgwWvmXXHFFey///707t2b7bffnuXLlzeqbTOz1kiVdKn96urqGD9+fLnDsBZs6tSpbLfddpm3O6+ZxVVWNu1S+O0XKk2+90bShIioLlNImXL+smLwRUQrW105zKcFzczMzDLk04JmrcT8+fPZb7/91pk+ZsyYta5nZWZmzePiyqyV2GSTTZg0aVK5wzAzW+8V9bSgpDMlTZH0qqQ/Smq5A0asxaikcYSW8HtiZq1J0YorSZsDPwKqI6I/0BY4sv61zJqnQ4cOLFy40P+ZV5CIYOHChXTo4O9WZtY6FPu0YBXQUdIKYAPg3SL3Z61cr169mDNnDgsWLMi03cWfrMi0vab6oGO7cofQJB06dKBXr17lDsPMrCSKVlxFxDuSrgJmA58A/4yIfxarPzOAdu3a0bdv38zbrZSfQ7eWnzeXm6RtgHtyJm0J/Dwiri1TSGbWghStuJK0MXAo0Bf4ELhP0rERcUet5UYCIwF69+5drHAqx1OXF6/tjC6YmVcLjvu5GQub3cxudUx/vvfIZrdtlSciXgd2BJDUFngHeLDelczMUsUc0L4vMDMiFkTECuBPwB61F4qIURFRHRHVPXv2LGI4ZmZNMgx4MyLeKncgZtYyFLO4mg3sJmkDJXd4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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "axs = volatility.plot()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In order to simplify the use of this class for the user, two convenience classes have been created to perform the main types of analyses with less parameters needed to be set by the user." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### SplitSeedVolatility \n", - "\n", - "The estimation of volatility is done in the same way as the default analysis described in TrainTestVolatility. The main advantage of using that class is a lower number of parameters to set." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
train_meantrain_stdtest_meantest_stddelta_meandelta_std
roc_auc0.8277960.0393560.8049260.0405010.022870.019264
\n", - "
" - ], - "text/plain": [ - " train_mean train_std test_mean test_std delta_mean delta_std\n", - "roc_auc 0.827796 0.039356 0.804926 0.040501 0.02287 0.019264" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "volatility = SplitSeedVolatility(clf, iterations=50, test_prc=0.5)\n", - "volatility.fit_compute(X, y)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### BootstrappedVolatility\n", - "\n", - "This class allows to perform a different experiment. At each iteration, the train-test split is the same, however, the samples in both splits are bootstrapped (sampled with replacement). Thus, some of the samples might be omitted, and some will be used multiple times in a given run. \n", - "\n", - "With this experiment, you can estimate an average performance for a specific train-test split, as well as indicate how volatile the scores are towards certain samples within your splits. Moreover, you can experiment with the amount of data sampled in each split, to tweak the test split size." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
train_meantrain_stdtest_meantest_stddelta_meandelta_std
accuracy0.8232000.0315670.7651200.0493030.0580800.034091
roc_auc0.8523160.0297620.7853780.0536470.0669380.038386
\n", - "
" - ], - "text/plain": [ - " train_mean train_std test_mean test_std delta_mean delta_std\n", - "accuracy 0.823200 0.031567 0.765120 0.049303 0.058080 0.034091\n", - "roc_auc 0.852316 0.029762 0.785378 0.053647 0.066938 0.038386" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "volatility = BootstrappedVolatility(clf, iterations=50, scoring=[\"accuracy\", \"roc_auc\"])\n", - "volatility.fit_compute(X, y)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/mkdocs.yml b/mkdocs.yml index 9943e2a3..28210ab4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: Probatus Docs +site_name: Probatus repo_url: https://github.com/ing-bank/probatus/ site_url: https://ing-bank.github.io/probatus/ @@ -7,38 +7,12 @@ site_author: ING Bank N. V. use_directory_urls: false -nav: - - Home: index.md - - Tutorials: - - ShapRFECV - Recursive Feature Elimination using SHAP importance: tutorials/nb_shap_feature_elimination.ipynb - - Tree-based & Linear Model Interpretation with SHAP: tutorials/nb_shap_model_interpreter.ipynb - - Imputation Strategy Comparison : tutorials/nb_imputation_comparison.ipynb - - Model Metrics Volatility: tutorials/nb_metric_volatility.ipynb - - Multivariate Sample Similarity: tutorials/nb_sample_similarity.ipynb - - Univariate Sample Similarity: tutorials/nb_distribution_statistics.ipynb - - Custom Scoring Metrics: tutorials/nb_custom_scoring.ipynb - - Binning options: tutorials/nb_binning.ipynb - - Explain Shapley Values: tutorials/nb_shap_dependence.ipynb - - HowTo: - - Reproduce the results: howto/reproducibility.ipynb - - Work with grouped data: howto/grouped_data.ipynb - - API: - - probatus.feature_elimination: api/feature_elimination.md - - probatus.interpret: api/model_interpret.md - - probatus.metric_volatility: api/metric_volatility.md - - probatus.missing_values : api/imputation_selector.md - - probatus.sample_similarity: api/sample_similarity.md - - probatus.stat_tests: api/stat_tests.md - - probatus.utils: api/utils.md - - Discussion: - - Vision: discussion/vision.md - - ShapRFECV vs sklearn RFECV: discussion/nb_rfecv_vs_shaprfecv.ipynb - - Contributing: discussion/contributing.md - watch: - - probatus +- probatus plugins: + - mkdocs-jupyter + - search - mkdocstrings: handlers: python: @@ -51,12 +25,6 @@ plugins: - "^__init__$" # but always include __init__ modules and methods rendering: show_root_toc_entry: false - - search - - mknotebooks: - enable_default_jupyter_cell_styling: true - enable_default_pandas_dataframe_styling: true - -copyright: Copyright © 2023 ING Bank N.V. theme: name: material @@ -72,16 +40,4 @@ theme: primary: deep orange accent: indigo - -markdown_extensions: - - codehilite - - pymdownx.highlight - - pymdownx.inlinehilite - - pymdownx.superfences - - pymdownx.details - - pymdownx.tabbed - - pymdownx.snippets - - pymdownx.highlight: - use_pygments: true - - toc: - permalink: true +copyright: Copyright © ING Bank N.V. \ No newline at end of file diff --git a/probatus/binning/__init__.py b/probatus/binning/__init__.py deleted file mode 100644 index bade5d31..00000000 --- a/probatus/binning/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -from .binning import SimpleBucketer, AgglomerativeBucketer, QuantileBucketer, TreeBucketer, Bucketer - -__all__ = ["SimpleBucketer", "AgglomerativeBucketer", "QuantileBucketer", "TreeBucketer", "Bucketer"] diff --git a/probatus/binning/binning.py b/probatus/binning/binning.py deleted file mode 100644 index 155fe86b..00000000 --- a/probatus/binning/binning.py +++ /dev/null @@ -1,470 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import warnings -from abc import abstractmethod - -import numpy as np -import pandas as pd -from sklearn.cluster import AgglomerativeClustering -from sklearn.tree import DecisionTreeClassifier, _tree -from sklearn.utils.validation import check_is_fitted - -from probatus.utils import ApproximationWarning, BaseFitComputeClass, assure_numpy_array - - -class Bucketer(BaseFitComputeClass): - """ - Bucket (bin) some data. - """ - - def __repr__(self): - """ - String representation. - """ - repr_ = f"{self.__class__.__name__}\n\tbincount: {self.bin_count}" - if hasattr(self, "boundaries_"): - repr_ += f"\nResults:\n\tcounts: {self.counts_}\n\tboundaries: {self.boundaries_}" - return repr_ - - @abstractmethod - def fit(self): - """ - Fit Bucketer. - """ - pass - - @property - def boundaries(self): - """ - The boundaries of the bins. - """ - msg = "The 'boundaries' attribute is deprecated, use 'boundaries_' instead." - msg += "The underscore suffix signals this is a fitted attribute." - warnings.warn( - msg, - DeprecationWarning, - ) - check_is_fitted(self) - return self.boundaries_ - - @property - def counts(self): - """ - Counts. - """ - msg = "The 'counts' attribute is deprecated, use 'counts_' instead." - msg += "The underscore suffix signals this is a fitted attribute." - warnings.warn(msg, DeprecationWarning) - check_is_fitted(self) - return self.counts_ - - def compute(self, X, y=None): - """ - Applies fitted bucketing algorithm on input data and counts number of samples per bin. - - Args: - X: (np.array) data to be bucketed - y: (np.array) ignored, for sklearn compatibility - - Returns: counts of the elements in X using the bucketing that was obtained by fitting the Bucketer instance - - """ - check_is_fitted(self) - - return self._compute_counts_per_bin(X, self.boundaries_) - - @staticmethod - def _compute_counts_per_bin(X, boundaries): - """ - Computes the counts per bin. - - Args: - X (np.array): data to be bucketed - boundaries (np.array): boundaries of the bins. - - Returns (np.array): Counts per bin. - """ - # np.digitize returns the indices of the bins to which each value in input array belongs - # the smallest value of the `boundaries` attribute equals the lowest value in the set the instance was - # fitted on, to prevent the smallest value of x_new to be in his own bucket, we ignore the first boundary - # value - bins = len(boundaries) - 1 - digitize_result = np.digitize(X, boundaries[1:], right=True) - result = pd.DataFrame({"bucket": digitize_result}).groupby("bucket")["bucket"].count() - # reindex the dataframe such that also empty buckets are included in the result - return result.reindex(np.arange(bins), fill_value=0).to_numpy() - - def fit_compute(self, X, y=None): - """ - Apply bucketing to new data and return number of samples per bin. - - Args: - X: (np.array) data to be bucketed - y: (np.array) One dimensional array, used if the target is needed for the bucketing. By default is set to - None - - Returns: counts of the elements in x_new using the bucketing that was obtained by fitting the Bucketer instance - - """ - self.fit(X, y) - return self.compute(X, y) - - @staticmethod - def _enforce_inf_boundaries(boundaries): - """ - This function ensures that the boundaries of the buckets are infinite. - - Arguments - boundaries: (list) List of bin boundaries. - - Returns: - (list): Boundaries with infinite edges - """ - boundaries[0] = -np.inf - boundaries[-1] = np.inf - return boundaries - - -class SimpleBucketer(Bucketer): - """ - Create equally spaced bins using numpy.histogram function. - - Example: - ```python - from probatus.binning import SimpleBucketer - - x = [1, 2, 1] - bins = 3 - myBucketer = SimpleBucketer(bin_count=bins) - myBucketer.fit(x) - ``` - - myBucketer.counts gives the number of elements per bucket - myBucketer.boundaries gives the boundaries of the buckets - """ - - def __init__(self, bin_count): - """ - Init. - """ - self.bin_count = bin_count - - @staticmethod - def simple_bins(x, bin_count, inf_edges=True): - """ - Simple bins. - """ - _, boundaries = np.histogram(x, bins=bin_count) - if inf_edges: - boundaries = Bucketer._enforce_inf_boundaries(boundaries) - - counts = Bucketer._compute_counts_per_bin(x, boundaries) - return counts, boundaries - - def fit(self, x, y=None): - """ - Fit bucketing on x. - - Args: - x: (np.array) Input array on which the boundaries of bins are fitted - y: (np.array) ignored. For sklearn-compatibility - - Returns: fitted bucketer object - """ - self.counts_, self.boundaries_ = self.simple_bins(x, self.bin_count) - return self - - -class AgglomerativeBucketer(Bucketer): - """ - Create binning by applying the Scikit-learn implementation of Agglomerative Clustering. - - Usage: - ```python - from probatus.binning import AgglomerativeBucketer - - x = [1, 2, 1] - bins = 3 - myBucketer = AgglomerativeBucketer(bin_count=bins) - myBucketer.fit(x) - ``` - - myBucketer.counts gives the number of elements per bucket - myBucketer.boundaries gives the boundaries of the buckets - """ - - def __init__(self, bin_count): - """ - Init. - """ - self.bin_count = bin_count - - @staticmethod - def agglomerative_clustering_binning(x, bin_count, inf_edges=True): - """ - Cluster. - """ - clustering = AgglomerativeClustering(n_clusters=bin_count).fit(np.asarray(x).reshape(-1, 1)) - df = pd.DataFrame({"x": x, "label": clustering.labels_}).sort_values(by="x") - cluster_minimum_values = df.groupby("label")["x"].min().sort_values().tolist() - cluster_maximum_values = df.groupby("label")["x"].max().sort_values().tolist() - # take the mean of the upper boundary of a cluster and the lower boundary of the next cluster - boundaries = [ - np.mean([cluster_minimum_values[i + 1], cluster_maximum_values[i]]) - for i in range(len(cluster_minimum_values) - 1) - ] - # add the lower boundary of the lowest cluster and the upper boundary of the highest cluster - boundaries = [cluster_minimum_values[0]] + boundaries + [cluster_maximum_values[-1]] - if inf_edges: - boundaries = Bucketer._enforce_inf_boundaries(boundaries) - counts = Bucketer._compute_counts_per_bin(x, boundaries) - return counts, boundaries - - def fit(self, x, y=None): - """ - Fit bucketing on x. - - Args: - x: (np.array) Input array on which the boundaries of bins are fitted - y: (np.array) ignored. For sklearn-compatibility - - Returns: fitted bucketer object - """ - self.counts_, self.boundaries_ = self.agglomerative_clustering_binning(x, self.bin_count) - return self - - -class QuantileBucketer(Bucketer): - """ - Create bins with equal number of elements. - - Usage: - ```python - from probatus.binning import QuantileBucketer - - x = [1, 2, 1] - bins = 3 - myBucketer = QuantileBucketer(bin_count=bins) - myBucketer.fit(x) - ``` - - myBucketer.counts gives the number of elements per bucket - myBucketer.boundaries gives the boundaries of the buckets - """ - - def __init__(self, bin_count): - """ - Init. - """ - self.bin_count = bin_count - - @staticmethod - def quantile_bins(x, bin_count, inf_edges=True): - """ - Bins. - """ - try: - out, boundaries = pd.qcut(x, q=bin_count, retbins=True, duplicates="raise") - except ValueError: - # If there are too many duplicate values (assume a lot of filled missing) - # this crashes - the exception drops them. - # This means that it will return approximate quantile bins - out, boundaries = pd.qcut(x, q=bin_count, retbins=True, duplicates="drop") - warnings.warn( - ApproximationWarning( - f"Unable to calculate quantile bins for this feature, because possibly " - f"there is too many duplicate values.Approximated quantiles, as a result," - f"the multiple boundaries have the same value. The number of bins has " - f"been lowered to {boundaries-1}. This can cause issue if you want to " - f"calculate the statistical test based on this binning. We suggest to " - f"retry with max number of bins of {boundaries-1} or apply different " - f"type of binning e.g. simple. If you run this functionality in AutoDist for multiple features, " - f"then you can decrease the bins only for that feature in a separate AutoDist run." - ) - ) - df = pd.DataFrame({"x": x}) - df["label"] = out - if inf_edges: - boundaries = Bucketer._enforce_inf_boundaries(boundaries) - counts = Bucketer._compute_counts_per_bin(x, boundaries) - return counts, boundaries - - def fit(self, x, y=None): - """ - Fit bucketing on x. - - Args: - x: (np.array) Input array on which the boundaries of bins are fitted - y: (np.array) ignored. For sklearn-compatibility - - Returns: fitted bucketer object - """ - self.counts_, self.boundaries_ = self.quantile_bins(x, self.bin_count) - return self - - -class TreeBucketer(Bucketer): - """ - Class for bucketing using Decision Trees. - - It returns the optimal buckets found by a one-dimensional Decision Tree relative to a binary target. - - Useful if the buckets be defined such that there is a substantial difference between the buckets in - the distribution of the target. - - Usage: - ```python - from probatus.binning import TreeBucketer - - x = [1, 2, 2, 5 ,3] - y = [0, 0 ,1 ,1 ,1] - myBucketer = TreeBucketer(inf_edges=True,max_depth=2,min_impurity_decrease=0.001) - myBucketer.fit(x,y) - ``` - - myBucketer.counts gives the number of elements per bucket - myBucketer.boundaries gives the boundaries of the buckets - - Args: - inf_edges (boolean): Flag to keep the lower and upper boundary as infinite (if set to True). - If false, the edges will be set to the minimum and maximum value of the fitted - - tree (sklearn.tree.DecisionTreeClassifier): decision tree object defined by the user. By default is None, and - it will be constructed using the provided **kwargs - - **tree_kwargs: kwargs related to the decision tree. - For and extensive list of parameters, please check the sklearn Decision Tree Classifier documentation - https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html - - The most relevant parameters useful for the bucketing, are listed below: - - - - criterion : {"gini", "entropy"}, default="gini" - The function to measure the quality of a split. Supported criteria are - "gini" for the Gini impurity and "entropy" for the information gain. - - - - max_depth : int, default=None - Defines the maximum theoretical number of bins (2^max_depth) - - The maximum depth of the tree. If None, then nodes are expanded until - all leaves are pure or until all leaves contain less than - min_samples_split samples. - - - - - min_samples_leaf : int or float, default=1 - Defines the minimum number of entries in each bucket. - - The minimum number of samples required to be at a leaf node. - A split point at any depth will only be considered if it leaves at - least ``min_samples_leaf`` training samples in each of the left and - right branches. This may have the effect of smoothing the model, - especially in regression. - - - If int, then consider `min_samples_leaf` as the minimum number. - - If float, then `min_samples_leaf` is a fraction and - `ceil(min_samples_leaf * n_samples)` are the minimum - number of samples for each node. - - .. versionchanged:: 0.18 - Added float values for fractions. - - - - min_impurity_decrease : float, default=0.0 - Controls the way the TreeBucketer splits. - When the criterion is set to 'entropy', the best results tend to - be achieved in the range [0.0001 - 0.01] - - A node will be split if this split induces a decrease of the impurity - greater than or equal to this value. - - The weighted impurity decrease equation is the following:: - - N_t / N * (impurity - N_t_R / N_t * right_impurity - - N_t_L / N_t * left_impurity) - - where ``N`` is the total number of samples, ``N_t`` is the number of - samples at the current node, ``N_t_L`` is the number of samples in the - left child, and ``N_t_R`` is the number of samples in the right child. - - ``N``, ``N_t``, ``N_t_R`` and ``N_t_L`` all refer to the weighted sum, - if ``sample_weight`` is passed. - - .. versionadded:: 0.19 - - """ - - def __init__(self, inf_edges=False, tree=None, **tree_kwargs): - """ - Init. - """ - self.bin_count = -1 - self.inf_edges = inf_edges - if tree is None: - self.tree = DecisionTreeClassifier(**tree_kwargs) - else: - self.tree = tree - - @staticmethod - def tree_bins(x, y, inf_edges, tree): - """ - Tree. - """ - X_in = assure_numpy_array(x).reshape(-1, 1) - y_in = assure_numpy_array(y).reshape(-1, 1) - tree.fit(X_in, y_in) - - if tree.min_samples_leaf >= X_in.shape[0]: - error_msg = ( - "Cannot Fit decision tree. min_samples_leaf must be < than the length of x.m" - + f"Currently min_samples_leaf {tree.min_samples_leaf} " - + f"and the length of X is {X_in.shape[0]}" - ) - raise ValueError(error_msg) - - leaves = tree.apply(X_in) - index, counts = np.unique(leaves, return_counts=True) - - bin_count = len(index) - - boundaries = np.unique(tree.tree_.threshold[tree.tree_.feature != _tree.TREE_UNDEFINED]) - boundaries = [np.min(X_in)] + boundaries.tolist() + [np.max(X_in)] - - if inf_edges: - boundaries[0] = -np.inf - boundaries[-1] = np.inf - - return counts.tolist(), boundaries, bin_count, tree - - def fit(self, X, y): - """ - Fit bucketing on x. - - Args: - x: (np.array) Input array on which the boundaries of bins are fitted - y: (np.array) optional, One dimensional array with the target. - - Returns: fitted bucketer object - """ - self.counts_, self.boundaries_, self.bin_count, self.tree = self.tree_bins(X, y, self.inf_edges, self.tree) - return self diff --git a/probatus/interpret/__init__.py b/probatus/interpret/__init__.py index 3e7c610b..a1f0eceb 100644 --- a/probatus/interpret/__init__.py +++ b/probatus/interpret/__init__.py @@ -18,8 +18,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -from .inspector import InspectorShap from .shap_dependence import DependencePlotter from .model_interpret import ShapModelInterpreter -__all__ = ["InspectorShap", "DependencePlotter", "ShapModelInterpreter"] +__all__ = ["DependencePlotter", "ShapModelInterpreter"] diff --git a/probatus/interpret/inspector.py b/probatus/interpret/inspector.py deleted file mode 100644 index cf8383e9..00000000 --- a/probatus/interpret/inspector.py +++ /dev/null @@ -1,542 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import copy - -import numpy as np -import pandas as pd -from sklearn.cluster import KMeans - -from probatus.utils import shap_helpers - -from ..utils import BaseFitComputeClass, NotFittedError, UnsupportedModelError - - -def return_confusion_metric(y_true, y_score, normalize=False): - """ - Computes a confusion metric as absolute difference between the y_true and y_score. - - If normalize is set to true, it will normalize y_score to the maximum value in the array - - Args: - y_true: (np.ndarray or pd.Series) true targets - y_score: (np.ndarray or pd.Series) model output - normalize: boolean, normalize or not to the maximum value - - Returns: (np.ndarray or pd.Series) confusion metric - - """ - - if normalize: - y_score = y_score / y_score.max() - - return np.abs(y_true - y_score) - - -class BaseInspector(BaseFitComputeClass): - """ - Base class. - """ - - def __init__(self, algotype, **kwargs): - """ - Init. - """ - self.algotype = algotype - if algotype == "kmeans": - self.clusterer = KMeans(**kwargs) - else: - raise UnsupportedModelError(f"The algorithm {algotype} is not supported") - - def __repr__(self): - """ - String representation. - """ - repr_ = f"{self.__class__.__name__},\n\t{self.algotype}" - if self.fitted: - repr_ += f"\n\tTotal clusters {np.unique(self.clusterer.labels_).shape[0]}" - return repr_ - - def fit_clusters(self, X): - """ - Perform the fit of the clusters with the algorithm specified in the constructor. - - Args: - X: input features - - Returns: cluster labels - """ - self.clusterer.fit(X) - self.fitted = True - - return self - - def predict_clusters(self, X): - """ - Predict clusters. - """ - if not self.fitted: - raise NotFittedError("Inspector not fitter. Run .fit()") - - labels = None - if self.algotype == "kmeans": - labels = self.clusterer.predict(X) - if self.algotype == "dbscan": - raise NotImplementedError("Implementation not finished (note the hdbscan package is not imported yet!)") - # labels, strengths = hdbscan.approximate_predict(self.clusterer, X) - - return labels - - @staticmethod - def assert_is_dataframe(df): - """ - Assertion. - """ - if isinstance(df, pd.DataFrame): - return df - - elif isinstance(df, np.ndarray) and len(df.shape) == 2: - return pd.DataFrame(df) - - else: - raise NotImplementedError("Sorry, X needs to be a pd.DataFrame for for a 2 dimensional numpy array") - - @staticmethod - def assert_is_series(series, index=None): - """ - Assert input is a pandas series. - """ - if isinstance(series, pd.Series): - return series - elif isinstance(series, pd.DataFrame) and series.shape[1] == 1: - return pd.Series(series.values.ravel(), index=series.index) - elif isinstance(series, np.ndarray) and len(series.shape) == 1 and index is not None: - return pd.Series(series, index=index) - else: - raise TypeError( - "The object should be a pd.Series, a dataframe with one column or a 1 dimensional numpy array" - ) - - -class InspectorShap(BaseInspector): - """ - Class to perform inspection of the model prediction based on Shapley values. - - It uses the calculated Shapley values for the train model to build clusters in the SHAP space. - For each cluster, an average confusion, average predicted probability and observed rate of a single class is - calculated. - Every sub cluster can be retrieved with the function slice_cluster to perform deeper analysis. - - The original dataframe indexing is used in slicing the dataframe, ensuring easy filtering - - Args: - model: (obj) pretrained model (with sklearn-like API) - algotype: (str) clustering algorithm (supported are kmeans and hdbscan) - confusion_metric: (str) Confusion metric to use: - - "proba": it will calculate the confusion metric as the absolute value of the target minus - the predicted probability. This provides a continuous measure of confusion, where 0 indicated - correct predictions and the closer the number is to 1, the higher the confusion - normalize_probability: (boolean) if true, it will normalize the probabilities to the max value when - computing the confusion metric - cluster_probabilities: (boolean) if true, uses the model prediction as an input for the cluster prediction - **kwargs: keyword arguments for the clustering algorithm - - """ - - def __init__( - self, - model, - algotype="kmeans", - confusion_metric="proba", - normalize_probability=False, - cluster_probability=False, - **kwargs, - ): - """ - Init. - """ - super().__init__(algotype, **kwargs) - self.model = model - self.isinspected = False - self.hasmultiple_dfs = False - self.normalize_proba = normalize_probability - self.cluster_probabilities = cluster_probability - self.agg_summary_df = None - self.set_names = None - self.confusion_metric = confusion_metric - self.cluster_report = None - self.y = None - self.predicted_proba = None - self.X_shap = None - self.clusters = None - self.init_eval_set_report_variables() - - if confusion_metric not in ["proba"]: - # TODO implement the target method - raise NotImplementedError(f"confusion metric {confusion_metric} not supported. See docstrings") - - def __repr__(self): - """ - String representation. - """ - repr_ = f"{self.__class__.__name__},\n\t{self.algotype}" - if self.fitted: - repr_ += f"\n\tTotal clusters {np.unique(self.clusterer.labels_).shape[0]}" - return repr_ - - def init_eval_set_report_variables(self): - """ - Init report values. - """ - self.X_shaps = list() - self.clusters_list = list() - self.ys = list() - self.predicted_probas = list() - - def compute_probabilities(self, X): - """ - Compute the probabilities for the model using the sklearn API. - - Args: - X: Feature set - - Returns: (np.array) probability - """ - return self.model.predict_proba(X)[:, 1] - - def fit_clusters(self, X): - """ - Perform the fit of the clusters with the algorithm specified in the constructor. - - Args: - X: input features - """ - X = copy.deepcopy(X) - - if self.cluster_probabilities: - X["probs"] = self.predicted_proba - - return super().fit_clusters(X) - - def predict_clusters(self, X): - """ - Predicts the clusters of the dataset X. - - Args: - X: features - - Returns: cluster labels - """ - X = copy.deepcopy(X) - - if self.cluster_probabilities: - X["probs"] = self.predicted_proba - - return super().predict_clusters(X) - - def fit(self, X, y=None, eval_set=None, sample_names=None, **shap_kwargs): - """ - Fits and orchestrates the cluster calculations. - - Args: - X: (pd.DataFrame) with the features set used to train the model - y: (pd.Series, default=None): targets used to train the model - eval_set: (list, default=None). list of tuples in the shape (X,y) containing evaluation samples, for example - a test sample, validation sample etc... X corresponds to the feature set of the sample, y corresponds - to the targets of the samples - sample_names: (list of strings, default=None): list of suffixed for the samples. - If none, it will be labelled with - sample_{i}, where i corresponds to the index of the sample. - List length must match that of eval_set - **shap_kwargs: kwargs to pass to the Shapley Tree Explained - """ - self.set_names = sample_names - if sample_names is not None: - # Make sure that the amount of eval sets matches the set names - assert len(eval_set) == len(sample_names), "set_names must be the same length as eval_set" - - ( - self.y, - self.predicted_proba, - self.X_shap, - self.clusters, - ) = self.perform_fit_calc(X=X, y=y, fit_clusters=True, **shap_kwargs) - if eval_set is not None: - assert isinstance(eval_set, list), "eval_set needs to be a list" - - self.hasmultiple_dfs = True - # Reset lists in case inspect run multiple times - self.init_eval_set_report_variables() - - for X_, y_ in eval_set: - y_, predicted_proba_, X_shap_, clusters_ = self.perform_fit_calc( - X=X_, y=y_, fit_clusters=False, **shap_kwargs - ) - - self.X_shaps.append(X_shap_) - self.ys.append(y_) - self.predicted_probas.append(predicted_proba_) - self.clusters_list.append(clusters_) - - return self - - def perform_fit_calc(self, X, y, fit_clusters=False, **shap_kwargs): - """ - Performs cluster calculations for a specific X and y. - - Args: - X: pd.DataFrame with the features set used to train the model - y: pd.Series (default None): targets used to train the model - fit_clusters: flag indicating whether clustering algorithm should be trained with computed shap values - **shap_kwargs: kwargs to pass to the Shapley Tree Explained - """ - X = self.assert_is_dataframe(X) - y = self.assert_is_series(y, index=X.index) - - # Compute probabilities for the input X using model - predicted_proba = pd.Series(self.compute_probabilities(X), index=y.index, name="pred_proba") - - # Compute SHAP values and cluster them - X_shap = shap_helpers.shap_to_df(self.model, X, **shap_kwargs) - if fit_clusters: - self.fit_clusters(X_shap) - clusters = pd.Series(self.predict_clusters(X_shap), index=y.index, name="cluster_id") - return y, predicted_proba, X_shap, clusters - - def _compute_report(self): - """ - Helper function to compute the report of the inspector. - - Performs aggregations per cluster id - """ - self.summary_df = self.create_summary_df( - self.clusters, self.y, self.predicted_proba, normalize=self.normalize_proba - ) - self.agg_summary_df = self.aggregate_summary_df(self.summary_df) - - if self.hasmultiple_dfs: - self.summary_dfs = [ - self.create_summary_df(clust, y, pred_proba, normalize=self.normalize_proba) - for clust, y, pred_proba in zip(self.clusters_list, self.ys, self.predicted_probas) - ] - - self.agg_summary_dfs = [self.aggregate_summary_df(df) for df in self.summary_dfs] - - def compute(self): - """ - Calculates a report containing the information per cluster. - - Includes the following: - - cluster id - - total number of observations in the cluster - - total number of target 1 in the cluster - - target 1 rate (ration of target 1 counts/observations) - - average predicted probabilities - - average confusion - - If multiple eval_sets were passed in the inspect() functions, the output will contain those aggregations as - well. The output names will use the sample names provided in the inspect function. Otherwise they will be - labelled by the suffix sample_{i}, where i is the index of the sample. - - Returns: (pd.DataFrame) with above mentioned aggregations. - """ - if self.cluster_report is not None: - return self.cluster_report - - self._compute_report() - out = copy.deepcopy(self.agg_summary_df) - - if self.hasmultiple_dfs: - for ix, agg_summary_df in enumerate(self.agg_summary_dfs): - if self.set_names is None: - sample_suffix = f"sample_{ix + 1}" - else: - sample_suffix = self.set_names[ix] - - out = pd.merge( - out, - agg_summary_df, - how="left", - on="cluster_id", - suffixes=("", f"_{sample_suffix}"), - ) - - self.cluster_report = out - return self.cluster_report - - def slice_cluster( - self, - cluster_id, - summary_df=None, - X_shap=None, - y=None, - predicted_proba=None, - complementary=False, - ): - """ - Slices the input dataframes by the cluster. - - Args: - cluster_id: (int or list for multiple cluster_id) cluster ids to to slice - summary_df: Optional parameter - the summary_df on which the masking should be performed. - if not passed the slicing is performed on summary generated by inspect method on X and y - X_shap: Optional parameter - the SHAP values generated from on X on which the masking should be performed. - if not passed the slicing is performed on X_shap generated by inspect method on X and y - y: Optional parameter - the y on which the masking should be performed. - if not passed the slicing is performed on y passed to inspect - predicted_proba: Optional parameter - the predicted_proba on which the masking should be performed. - if not passed the slicing is performed on predicted_proba generated by inspect method on X and y - complementary: flag that returns the cluster_id if set to False, otherwise the complementary dataframe (i.e. - those with ~mask) - - Returns: tuple: Dataframe of sliced Shapley values, series of sliced targets, sliced probabilities - """ - if self.cluster_report is None: - self.compute() - - # Check if input specified by user, otherwise use the ones from self - if summary_df is None: - summary_df = self.summary_df - if X_shap is None: - X_shap = self.X_shap - if y is None: - y = self.y - if predicted_proba is None: - predicted_proba = self.predicted_proba - - mask = self.get_cluster_mask(summary_df, cluster_id) - if not complementary: - return X_shap[mask], y[mask], predicted_proba[mask] - else: - return X_shap[~mask], y[~mask], predicted_proba[~mask] - - def slice_cluster_eval_set(self, cluster_id, complementary=False): - """ - Slices the input dataframes passed in the eval_set in the inspect function by the cluster id. - - Args: - cluster_id: (int or list for multiple cluster_id) cluster ids to to slice - complementary: flag that returns the cluster_id if set to False, otherwise the complementary dataframe (ie - those with ~mask) - - Returns: list of tuplse: each element of the list containst - Dataframe of sliced shapley values, series of sliced targets, sliced probabilities - """ - if not self.hasmultiple_dfs: - raise NotFittedError("You did not fit the eval set. Please add an eval set when calling inspect()") - - output = [] - for X_shap, y, predicted_proba, summary_df in zip( - self.X_shaps, self.ys, self.predicted_probas, self.summary_dfs - ): - output.append( - self.slice_cluster( - cluster_id=cluster_id, - summary_df=summary_df, - X_shap=X_shap, - y=y, - predicted_proba=predicted_proba, - complementary=complementary, - ) - ) - return output - - @staticmethod - def get_cluster_mask(df, cluster_id): - """ - Returns the mask to filter the cluster id. - - Args: - df: dataframe with 'cluster_id' in it - cluster_id: int or list of cluster ids to mask - """ - if not isinstance(cluster_id, list): - cluster_id = [cluster_id] - - mask = df["cluster_id"].isin(cluster_id) - return mask - - @staticmethod - def create_summary_df(cluster, y, probas, normalize=False): - """ - Creates a summary. - - by concatenating the cluster series, the targets, the probabilities and the measured confusion. - - Args: - cluster: pd.Series of clusters - y: pd.Series of targets - probas: pd.Series of predicted probabilities of the model - normalize: boolean (if the predicted probabilities should be normalized to the max value - - Returns: pd.DataFrame (concatenation of the inputs) - """ - confusion = return_confusion_metric(y, probas, normalize=normalize).rename("confusion") - - summary = [cluster, y.rename("target"), probas, confusion] - - return pd.concat(summary, axis=1) - - @staticmethod - def aggregate_summary_df(df): - """ - Performs the aggregations at the cluster_id level needed to generate the report of the inspection. - - Args: - df: input df to aggregate - - Returns: pd.Dataframe with aggregation results - """ - out = ( - df.groupby("cluster_id") - .agg( - total_label_1=pd.NamedAgg(column="target", aggfunc="sum"), - total_entries=pd.NamedAgg(column="target", aggfunc="count"), - label_1_rate=pd.NamedAgg(column="target", aggfunc="mean"), - average_confusion=pd.NamedAgg(column="confusion", aggfunc="mean"), - average_pred_proba=pd.NamedAgg(column="pred_proba", aggfunc="mean"), - ) - .reset_index() - .rename(columns={"index": "cluster_id"}) - .sort_values(by="cluster_id") - ) - - return out - - def fit_compute(self, X, y=None, eval_set=None, sample_names=None, **shap_kwargs): - """ - Fits and orchestrates the cluster calculations and returns the computed report. - - Args: - X: (pd.DataFrame) with the features set used to train the model - y: (pd.Series, default=None): targets used to train the model - eval_set: (list, default=None). list of tuples in the shape (X,y) containing evaluation samples, for example - a test sample, validation sample etc... X corresponds to the feature set of the sample, y corresponds - to the targets of the samples - sample_names: (list of strings, default=None): list of suffixed for the samples. If none, it will be - labelled with sample_{i}, where i corresponds to the index of the sample. - List length must match that of eval_set - **shap_kwargs: kwargs to pass to the Shapley Tree Explained - - Returns: - (pd.DataFrame) Report with aggregations described in compute() method. - """ - self.fit(X, y, eval_set, sample_names, **shap_kwargs) - return self.compute() diff --git a/probatus/interpret/shap_dependence.py b/probatus/interpret/shap_dependence.py index d930004b..e809f89f 100644 --- a/probatus/interpret/shap_dependence.py +++ b/probatus/interpret/shap_dependence.py @@ -21,8 +21,8 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd +from sklearn.preprocessing import KBinsDiscretizer -from probatus.binning import AgglomerativeBucketer, QuantileBucketer, SimpleBucketer from probatus.utils import BaseFitComputePlotClass, preprocess_data, preprocess_labels, shap_to_df @@ -46,7 +46,7 @@ class DependencePlotter(BaseFitComputePlotClass): bdp = DependencePlotter(clf) shap_values = bdp.fit_compute(X, y) - bdp.plot(feature=2, type_binning='simple') + bdp.plot(feature=2) ``` @@ -171,7 +171,6 @@ def plot( feature, figsize=(15, 10), bins=10, - type_binning="simple", show=True, min_q=0, max_q=1, @@ -190,9 +189,6 @@ def plot( bins (int or list[float]): Number of bins or boundaries of bins (supplied in list) for target-rate plot. - type_binning ({'simple', 'agglomerative', 'quantile'}): - Type of binning to be used in target-rate plot (see :mod:`binning` for more information). - show (bool, optional): If True, the plots are showed to the user, otherwise they are not shown. Not showing plot can be useful, when you want to edit the returned axis, before showing it. @@ -215,8 +211,6 @@ def plot( raise ValueError("min_q must be smaller than max_q") if feature not in self.X.columns: raise ValueError("Feature not recognized") - if type_binning not in ["simple", "agglomerative", "quantile"]: - raise ValueError("Select one of the following binning methods: 'simple', 'agglomerative', 'quantile'") if (alpha < 0) or (alpha > 1): raise ValueError("alpha must be a float value between 0 and 1") @@ -227,7 +221,7 @@ def plot( ax2 = plt.subplot2grid((3, 1), (2, 0)) self._dependence_plot(feature=feature, ax=ax1) - self._target_rate_plot(feature=feature, bins=bins, type_binning=type_binning, ax=ax2) + self._target_rate_plot(feature=feature, bins=bins, ax=ax2) ax2.set_xlim(ax1.get_xlim()) @@ -268,7 +262,7 @@ def _dependence_plot(self, feature, ax=None): return ax - def _target_rate_plot(self, feature, bins=10, type_binning="simple", ax=None): + def _target_rate_plot(self, feature, bins=10, ax=None): """ Plots the distributions of the specific features, as well as the target rate as function of the feature. @@ -279,9 +273,6 @@ def _target_rate_plot(self, feature, bins=10, type_binning="simple", ax=None): bins (int or list[float]), optional: Number of bins or boundaries of desired bins in list. - type_binning ({'simple', 'agglomerative', 'quantile'}, optional): - Type of binning strategy used to create bins. - ax (matplotlib.pyplot.axes, optional): Optional axis on which to draw plot. @@ -294,12 +285,11 @@ def _target_rate_plot(self, feature, bins=10, type_binning="simple", ax=None): # Create bins if not explicitly supplied if isinstance(bins, int): - if type_binning == "simple": - counts, bins = SimpleBucketer.simple_bins(x, bins) - elif type_binning == "agglomerative": - counts, bins = AgglomerativeBucketer.agglomerative_clustering_binning(x, bins) - elif type_binning == "quantile": - counts, bins = QuantileBucketer.quantile_bins(x, bins) + simple_binner = KBinsDiscretizer(n_bins=bins, encode="ordinal", strategy="uniform").fit( + np.array(x).reshape(-1, 1) + ) + bins = simple_binner.bin_edges_[0] + bins[0], bins[-1] = -np.inf, np.inf # Determine bin for datapoints bins[-1] = bins[-1] + 1 diff --git a/probatus/metric_volatility/__init__.py b/probatus/metric_volatility/__init__.py deleted file mode 100644 index a034a7ec..00000000 --- a/probatus/metric_volatility/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -from .metric import get_metric -from .volatility import ( - BaseVolatilityEstimator, - TrainTestVolatility, - BootstrappedVolatility, - SplitSeedVolatility, -) -from .utils import sample_data, check_sampling_input - -__all__ = [ - "get_metric", - "BaseVolatilityEstimator", - "TrainTestVolatility", - "BootstrappedVolatility", - "SplitSeedVolatility", - "sample_data", - "check_sampling_input", -] diff --git a/probatus/metric_volatility/metric.py b/probatus/metric_volatility/metric.py deleted file mode 100644 index e246bed7..00000000 --- a/probatus/metric_volatility/metric.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import numpy as np -import pandas as pd -from sklearn.model_selection import train_test_split - -from probatus.metric_volatility.utils import sample_data -from probatus.utils import assure_numpy_array - - -def get_metric( - X, - y, - clf, - test_size, - split_seed, - scorers, - train_sampling_type=None, - test_sampling_type=None, - train_sampling_fraction=1, - test_sampling_fraction=1, -): - """ - Draws random train/test sample from the data using random seed and calculates metric of interest. - - Args: - X (np.array or pd.DataFrame): - Dataset with features. - - y (np.array or pd.Series): - Target of the prediction. - - clf (model object): - Binary classification model or pipeline. - - test_size (float): - Fraction of data used for testing the model. - - split_seed (int): - Randomized seed used for splitting data. - - scorers (list of Scorers): - List of Scorer objects used to score the trained model. - - train_sampling_type (str, optional): - String indicating what type of sampling should be applied on train set: - - - `None`: indicates that no additional sampling is done after splitting data, - - `'bootstrap'`: indicates that sampling with replacement will be performed on train data, - - `'subsample'`: indicates that sampling without repetition will be performed on train data. - - test_sampling_type (str, optional): - string indicating what type of sampling should be applied on test set: - - - `None`: indicates that no additional sampling is done after splitting data - - `'bootstrap'`: indicates that sampling with replacement will be performed on test data - - `'subsample'`: indicates that sampling without repetition will be performed on test data - - train_sampling_fraction (float, optional): - Fraction of train data sampled, if sample_train_type is not None. Default value is 1. - - test_sampling_fraction (float, optional): - Fraction of test data sampled, if sample_test_type is not None. Default value is 1. - - Returns: - (pd.Dataframe): - Dataframe with results for a given model trained. Rows indicate the metric measured and columns their - results. - """ - - if not (isinstance(X, np.ndarray) or isinstance(X, pd.DataFrame)): - X = assure_numpy_array(X) - if not (isinstance(X, np.ndarray) or isinstance(X, pd.Series)): - y = assure_numpy_array(y) - - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=split_seed, stratify=y) - - # Sample data based on the input arguments - X_train, y_train = sample_data( - X=X_train, - y=y_train, - sampling_type=train_sampling_type, - sampling_fraction=train_sampling_fraction, - dataset_name="train", - ) - X_test, y_test = sample_data( - X=X_test, - y=y_test, - sampling_type=test_sampling_type, - sampling_fraction=test_sampling_fraction, - dataset_name="test", - ) - - clf = clf.fit(X_train, y_train) - - results_columns = ["metric_name", "train_score", "test_score", "delta_score"] - results = [] - - for scorer in scorers: - score_train = scorer.score(clf, X_train, y_train) - score_test = scorer.score(clf, X_test, y_test) - score_delta = score_train - score_test - - results.append( - [scorer.metric_name, score_train, score_test, score_delta], - ) - return pd.DataFrame(results, columns=results_columns) diff --git a/probatus/metric_volatility/utils.py b/probatus/metric_volatility/utils.py deleted file mode 100644 index dbdeb5e8..00000000 --- a/probatus/metric_volatility/utils.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import numpy as np -import pandas as pd - - -def sample_data(X, y, sampling_type, sampling_fraction, dataset_name="dataset"): - """ - Sample data. - """ - check_sampling_input(sampling_type, sampling_fraction, dataset_name) - - if sampling_type is None: - return X, y - - number_of_samples = np.ceil(sampling_fraction * X.shape[0]).astype(int) - array_index = list(range(X.shape[0])) - - if sampling_type == "bootstrap": - rows_indexes = np.random.choice(array_index, number_of_samples, replace=True) - else: - if sampling_fraction == 1 or number_of_samples == X.shape[0]: - return X, y - else: - rows_indexes = np.random.choice(array_index, number_of_samples, replace=True) - - # Get output correctly based on the type - if isinstance(X, pd.DataFrame): - output_X = X.iloc[rows_indexes] - else: - output_X = X[rows_indexes] - if isinstance(y, pd.DataFrame): - output_y = y.iloc[rows_indexes] - else: - output_y = y[rows_indexes] - - return output_X, output_y - - -def check_sampling_input(sampling_type, fraction, dataset_name): - """ - Check. - """ - if sampling_type is not None: - if sampling_type == "bootstrap": - if fraction <= 0: - raise (ValueError(f"For bootstrapping {dataset_name} fraction needs to be above 0")) - elif sampling_type == "subsample": - if fraction <= 0 or fraction >= 1: - raise (ValueError(f"For bootstrapping {dataset_name} fraction needs to be be above 0 and below 1")) - else: - raise (ValueError("This sampling method is not implemented")) diff --git a/probatus/metric_volatility/volatility.py b/probatus/metric_volatility/volatility.py deleted file mode 100644 index 3d004dcc..00000000 --- a/probatus/metric_volatility/volatility.py +++ /dev/null @@ -1,759 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import warnings - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from joblib import Parallel, delayed -from tqdm.auto import tqdm - -from probatus.metric_volatility.metric import get_metric -from probatus.metric_volatility.utils import check_sampling_input -from probatus.stat_tests import DistributionStatistics -from probatus.utils import ( - BaseFitComputePlotClass, - assure_list_of_strings, - assure_list_values_allowed, - get_scorers, - preprocess_data, - preprocess_labels, -) - - -class BaseVolatilityEstimator(BaseFitComputePlotClass): - """ - Base object for estimating volatility estimation. - - This class is a base class, therefore cannot be used on its - own. Implements common API that can be used by all subclasses. - """ - - def __init__( - self, - clf, - scoring="roc_auc", - test_prc=0.25, - n_jobs=1, - stats_tests_to_apply=None, - verbose=0, - random_state=None, - ): - """ - Initializes the class. - - Args: - clf (model object): - Binary classification model or pipeline. - - scoring (string, list of strings, probatus.utils.Scorer or list of probatus.utils.Scorers, optional): - Metrics for which the score is calculated. It can be either a name or list of names metric names and - needs to be aligned with predefined classification scorers names in sklearn - ([link](https://scikit-learn.org/stable/modules/model_evaluation.html)). - Another option is using probatus.utils.Scorer to define a custom metric. - - test_prc (float, optional): - Percentage of input data used as test. By default 0.25. - - n_jobs (int, optional): - Number of parallel executions. If -1 use all available cores. By default 1. - - stats_tests_to_apply (str or list of str, optional): - Test or list of tests to apply. Available tests: - - - `'ES'`: Epps-Singleton - - `'KS'`: Kolmogorov-Smirnov - - `'PSI'`: Population Stability Index - - `'SW'`: Shapiro-Wilk - - `'AD'`: Anderson-Darling - - Details on the available tests can be found [here](/probatus/api/stat_tests.html#available-tests). - - verbose (int, optional): - Controls verbosity of the output: - - - 0 - neither prints nor warnings are shown - - 1 - 50 - only most important warnings and indication of progress in fitting the object. - - 51 - 100 - shows other warnings and prints - - above 100 - presents all prints and all warnings (including SHAP warnings). - - random_state (int, optional): - Random state set at each round of feature elimination. If it is None, the results will not be - reproducible and in random search at each iteration a different hyperparameters might be tested. For - reproducible results set it to integer. - """ - self.clf = clf - self.n_jobs = n_jobs - self.random_state = random_state - self.test_prc = test_prc - self.iterations_results = None - self.report = None - self.verbose = verbose - self.allowed_stats_tests = list(DistributionStatistics.statistical_test_dict.keys()) - - # TODO set reasonable default value for the parameter, to choose the statistical test for the user for different - # ways to compute volatility - if stats_tests_to_apply is not None: - self.stats_tests_to_apply = assure_list_of_strings(stats_tests_to_apply, "stats_tests_to_apply") - assure_list_values_allowed( - variable=self.stats_tests_to_apply, - variable_name="stats_tests_to_apply", - allowed_values=self.allowed_stats_tests, - ) - else: - self.stats_tests_to_apply = [] - - self.stats_tests_objects = [] - if len(self.stats_tests_to_apply) > 0: - if self.verbose > 0: - warnings.warn( - "Computing statistics for distributions is an experimental feature. While using it, keep " - "in mind that the samples of metrics might be correlated." - ) - for test_name in self.stats_tests_to_apply: - self.stats_tests_objects.append(DistributionStatistics(statistical_test=test_name)) - - self.scorers = get_scorers(scoring) - - def fit(self, *args, **kwargs): - """ - Base fit functionality that should be executed before each fit. - - Returns: - (BaseVolatilityEstimator): - Fitted object. - """ - # Set seed for results reproducibility - if self.random_state is not None: - np.random.seed(self.random_state) - - # Initialize the report and results - self.iterations_results = None - self.report = None - self.fitted = True - return self - - def compute(self, metrics=None): - """ - Reports the statistics. - - Args: - metrics (str or list of strings, optional): - Name or list of names of metrics to be plotted. If not all metrics are presented. - - Returns: - (pandas.Dataframe): - Report that contains the evaluation mean and std on train and test sets for each metric. - """ - self._check_if_fitted() - if self.report is None: - raise ( - ValueError( - "Report is None, thus it has not been computed by fit method. Please extend the " - "BaseVolatilityEstimator class, overwrite fit method, and within fit run compute_report()" - ) - ) - - if metrics is None: - return self.report - else: - if not isinstance(metrics, list): - metrics = [metrics] - return self.report.loc[metrics] - - def plot( - self, - metrics=None, - bins=10, - show=True, - height_per_subplot=5, - width_per_subplot=5, - ): - """ - Plots distribution of the metric. - - Args: - metrics (str or list of strings, optional): - Name or list of names of metrics to be plotted. If not all metrics are presented. - - bins (int, optional): - Number of bins into which histogram is built. - - show (bool, optional): - If True, the plots are showed to the user, otherwise they are not shown. Not showing plot can be useful, - when you want to edit the returned axis, before showing it. - - height_per_subplot (int, optional): - Height of each subplot. Default is 5. - - width_per_subplot (int, optional): - Width of each subplot. Default is 5. - - Returns - (list(matplotlib.axes)): - Axes that include the plot. - """ - - target_report = self.compute(metrics=metrics) - - if target_report.shape[0] >= 1: - fig, axs = plt.subplots( - target_report.shape[0], - 2, - figsize=( - width_per_subplot * 2, - height_per_subplot * target_report.shape[0], - ), - ) - - # Enable traversing the axs - axs = axs.flatten() - axis_index = 0 - - for metric, row in target_report.iterrows(): - train, test, delta = self._get_samples_to_plot(metric_name=metric) - - axs[axis_index].hist(train, alpha=0.5, label=f"Train {metric}", bins=bins) - axs[axis_index].hist(test, alpha=0.5, label=f"Test {metric}", bins=bins) - axs[axis_index].set_title(f"Distributions {metric}") - axs[axis_index].legend(loc="upper right") - - axs[axis_index + 1].hist(delta, alpha=0.5, label=f"Delta {metric}", bins=bins) - axs[axis_index + 1].set_title(f"Distributions delta {metric}") - axs[axis_index + 1].legend(loc="upper right") - - axis_index += 2 - - for ax in axs.flat: - ax.set(xlabel=f"{metric} score", ylabel="Results count") - - if show: - plt.show() - else: - plt.close() - - return axs - - def _get_samples_to_plot(self, metric_name): - """ - Selects samples to be plotted. - - Args: - metric_name (str): - Name of metric for which the data should be selected. - """ - current_metric_results = self.iterations_results[self.iterations_results["metric_name"] == metric_name] - train = current_metric_results["train_score"] - test = current_metric_results["test_score"] - delta = current_metric_results["delta_score"] - - return train, test, delta - - def _create_report(self): - """ - Create a report. - - Based on the results for each metric for different sampling, mean and std of distributions of all metrics and - store them as report. - """ - unique_metrics = self.iterations_results["metric_name"].unique() - - # Get columns which will be filled - stats_tests_columns = [] - for stats_tests_object in self.stats_tests_objects: - stats_tests_columns.append(f"{stats_tests_object.statistical_test_name} statistic") - stats_tests_columns.append(f"{stats_tests_object.statistical_test_name} p-value") - stats_columns = [ - "train_mean", - "train_std", - "test_mean", - "test_std", - "delta_mean", - "delta_std", - ] - report_columns = stats_columns + stats_tests_columns - - report = [] - - for metric in unique_metrics: - metric_iterations_results = self.iterations_results[self.iterations_results["metric_name"] == metric] - metrics = self._compute_mean_std_from_runs(metric_iterations_results) - stats_tests_values = self._compute_stats_tests_values(metric_iterations_results) - metric_row = metrics + stats_tests_values - report.append(metric_row) - - self.report = pd.DataFrame(report, columns=report_columns, index=unique_metrics) - - def _compute_mean_std_from_runs(self, metric_iterations_results): - """ - Compute mean and std of results. - - Args: - metric_iterations_results (pandas.DataFrame): - Scores for a single metric for each iteration. - - Returns: - (list): - List containing mean and std of train, test and deltas. - """ - train_mean_score = np.mean(metric_iterations_results["train_score"]) - test_mean_score = np.mean(metric_iterations_results["test_score"]) - delta_mean_score = np.mean(metric_iterations_results["delta_score"]) - train_std_score = np.std(metric_iterations_results["train_score"]) - test_std_score = np.std(metric_iterations_results["test_score"]) - delta_std_score = np.std(metric_iterations_results["delta_score"]) - return [ - train_mean_score, - train_std_score, - test_mean_score, - test_std_score, - delta_mean_score, - delta_std_score, - ] - - def _compute_stats_tests_values(self, metric_iterations_results): - """ - Compute statistics and p-values of specified tests. - - Args: - metric_iterations_results (pandas.DataFrame): - Scores for a single metric for each iteration. - - Returns: - (list): - List containing statistics and p-values of distributions. - """ - statistics = [] - for stats_test in self.stats_tests_objects: - stats, p_value = stats_test.compute( - metric_iterations_results["test_score"], - metric_iterations_results["train_score"], - ) - statistics += [stats, p_value] - return statistics - - def fit_compute(self, *args, **kwargs): - """ - Fit compute. - - Runs trains and evaluates a number of models on train and test sets extracted using different random seeds. - Reports the statistics of the selected metric. - - Takes as arguments the same parameters as fit() method. - - Returns: - (pandas.Dataframe): - Report that contains the evaluation mean and std on train and test sets for each metric. - """ - self.fit(*args, **kwargs) - return self.compute() - - -class TrainTestVolatility(BaseVolatilityEstimator): - """ - Estimation of volatility of metrics. - - The estimation is done by splitting the data into train and test multiple times - and training and scoring a model based on these metrics. The class allows for choosing whether at each iteration - the train test split should be the same or different, whether and how the train and test sets should be sampled. - - Examples: - - ```python - from sklearn.datasets import make_classification - from sklearn.ensemble import RandomForestClassifier - from probatus.metric_volatility import TrainTestVolatility - X, y = make_classification(n_features=4) - clf = RandomForestClassifier() - volatility = TrainTestVolatility(clf, iterations=10 , test_prc = 0.5) - volatility_report = volatility.fit_compute(X, y) - volatility.plot() - ``` - - - """ - - def __init__( - self, - clf, - iterations=1000, - scoring="roc_auc", - sample_train_test_split_seed=True, - train_sampling_type=None, - test_sampling_type=None, - train_sampling_fraction=1, - test_sampling_fraction=1, - test_prc=0.25, - n_jobs=1, - stats_tests_to_apply=None, - verbose=0, - random_state=None, - ): - """ - Initializes the class. - - Args: - clf (model object): - Binary classification model or pipeline. - - iterations (int, optional): - Number of iterations in seed bootstrapping. By default 1000. - - scoring (string, list of strings, probatus.utils.Scorer or list of probatus.utils.Scorers, optional): - Metrics for which the score is calculated. It can be either a name or list of names metric names and - needs to be aligned with predefined classification scorers names in sklearn - ([link](https://scikit-learn.org/stable/modules/model_evaluation.html)). - Another option is using probatus.utils.Scorer to define a custom metric. - - sample_train_test_split_seed (bool, optional): - Flag indicating whether each train test split should be done - randomly or measurement should be done for single split. Default is True, which indicates that each. - iteration is performed on a random train test split. If the value is False, the random_seed for the - split is set to train_test_split_seed. - - train_sampling_type (str, optional): - String indicating what type of sampling should be applied on train set: - - - `None` indicates that no additional sampling is done after splitting data, - - `'bootstrap'` indicates that sampling with replacement will be performed on train data, - - `'subsample'` indicates that sampling without repetition will be performed on train data. - - test_sampling_type (str, optional): - String indicating what type of sampling should be applied on test set: - - - `None` indicates that no additional sampling is done after splitting data, - - `'bootstrap'` indicates that sampling with replacement will be performed on test data, - - `'subsample'` indicates that sampling without repetition will be performed on test data. - - train_sampling_fraction (float, optional): - Fraction of train data sampled, if sample_train_type is not None. - Default value is 1. - - test_sampling_fraction (float, optional): - Fraction of test data sampled, if sample_test_type is not None. Default value is 1. - - test_prc (float, optional): - Percentage of input data used as test. By default 0.25. - - n_jobs (int, optional): - Number of parallel executions. If -1 use all available cores. By default 1. - - stats_tests_to_apply (str or list of str, optional): - List of tests to apply, default is None. Available options: - - - `'ES'`: Epps-Singleton - - `'KS'`: Kolmogorov-Smirnov - - `'PSI'`: Population Stability Index - - `'SW'`: Shapiro-Wilk - - `'AD'`: Anderson-Darling - - Details on the available tests can be found [here](/probatus/api/stat_tests.html#available-tests). - - verbose (int, optional): - Controls verbosity of the output: - - - 0 - neither prints nor warnings are shown - - 1 - 50 - only most important warnings - - 51 - 100 - shows other warnings and prints - - above 100 - presents all prints and all warnings (including SHAP warnings). - - random_state (int, optional): - Random state set at each round of feature elimination. If it is None, the results will not be - reproducible and in random search at each iteration a different hyperparameters might be tested. For - reproducible results set it to integer. - """ - super().__init__( - clf=clf, - scoring=scoring, - test_prc=test_prc, - n_jobs=n_jobs, - stats_tests_to_apply=stats_tests_to_apply, - verbose=verbose, - random_state=random_state, - ) - self.iterations = iterations - self.train_sampling_type = train_sampling_type - self.test_sampling_type = test_sampling_type - self.sample_train_test_split_seed = sample_train_test_split_seed - self.train_sampling_fraction = train_sampling_fraction - self.test_sampling_fraction = test_sampling_fraction - - check_sampling_input(train_sampling_type, train_sampling_fraction, "train") - check_sampling_input(test_sampling_type, test_sampling_fraction, "test") - - def fit(self, X, y, column_names=None): - """ - Fit. - - Bootstraps a number of random seeds, then splits the data based on the sampled seeds and estimates performance - of the model based on the split data. - - Args: - X (pandas.DataFrame or numpy.ndarray): - Array with samples and features. - - y (pandas.Series or numpy.ndarray): - Array with targets. - - column_names (list of str, optional): - List of feature names of the provided samples. If provided it will be used to overwrite the existing - feature names. If not provided the existing feature names are used or default feature names are - generated. - - Returns: - (TrainTestVolatility): - Fitted object. - """ - super().fit() - - self.X, self.column_names = preprocess_data(X, X_name="X", column_names=column_names, verbose=self.verbose) - self.y = preprocess_labels(y, y_name="y", index=self.X.index, verbose=self.verbose) - - if self.sample_train_test_split_seed: - random_seeds = np.random.random_integers(0, 999999, self.iterations) - else: - random_seeds = (np.ones(self.iterations)).astype(int) - if self.random_state: - random_seeds = random_seeds * self.random_state - - if self.verbose > 0: - random_seeds = tqdm(random_seeds) - - results_per_iteration = Parallel(n_jobs=self.n_jobs)( - delayed(get_metric)( - X=self.X, - y=self.y, - clf=self.clf, - test_size=self.test_prc, - split_seed=split_seed, - scorers=self.scorers, - train_sampling_type=self.train_sampling_type, - test_sampling_type=self.test_sampling_type, - train_sampling_fraction=self.train_sampling_fraction, - test_sampling_fraction=self.test_sampling_fraction, - ) - for split_seed in random_seeds - ) - - self.iterations_results = pd.concat(results_per_iteration, ignore_index=True) - - self._create_report() - return self - - -class SplitSeedVolatility(TrainTestVolatility): - """ - Estimation of volatility of metrics depending on the seed used to split the data. - - At every iteration it splits the - data into train and test set using a different stratified split and volatility of the metrics is calculated. - - Examples: - ```python - from sklearn.datasets import make_classification - from sklearn.ensemble import RandomForestClassifier - from probatus.metric_volatility import SplitSeedVolatility - X, y = make_classification(n_features=4) - clf = RandomForestClassifier() - volatility = SplitSeedVolatility(clf, iterations=10 , test_prc = 0.5) - volatility_report = volatility.fit_compute(X, y) - volatility.plot() - ``` - - - """ - - def __init__( - self, - clf, - iterations=1000, - scoring="roc_auc", - test_prc=0.25, - n_jobs=1, - stats_tests_to_apply=None, - verbose=0, - random_state=None, - ): - """ - Initializes the class. - - Args: - clf (model object): - Binary classification model or pipeline. - - iterations (int, optional): - Number of iterations in seed bootstrapping. By default 1000. - - scoring (string, list of strings, probatus.utils.Scorer or list of probatus.utils.Scorers, optional): - Metrics for which the score is calculated. It can be either a name or list of names metric names and - needs to be aligned with predefined classification scorers names in sklearn - ([link](https://scikit-learn.org/stable/modules/model_evaluation.html)). - Another option is using probatus.utils.Scorer to define a custom metric. - - test_prc (float, optional): - Percentage of input data used as test. By default 0.25. - - n_jobs (int, optional): - Number of parallel executions. If -1 use all available cores. By default 1. - - stats_tests_to_apply (None, string or list of str, optional): - List of tests to apply, default is None. Available options: - - - `'ES'`: Epps-Singleton - - `'KS'`: Kolmogorov-Smirnov - - `'PSI'`: Population Stability Index - - `'SW'`: Shapiro-Wilk - - `'AD'`: Anderson-Darling - - Details on the available tests can be found [here](/probatus/api/stat_tests.html#available-tests). - - verbose (int, optional): - Controls verbosity of the output: - - - 0 - neither prints nor warnings are shown - - 1 - 50 - only most important warnings - - 51 - 100 - shows other warnings and prints - - above 100 - presents all prints and all warnings (including SHAP warnings). - - random_state (int, optional): - Random state set at each round of feature elimination. If it is None, the results will not be - reproducible and in random search at each iteration a different hyperparameters might be tested. For - reproducible results set it to integer. - """ - super().__init__( - clf=clf, - sample_train_test_split_seed=True, - train_sampling_type=None, - test_sampling_type=None, - train_sampling_fraction=1, - test_sampling_fraction=1, - iterations=iterations, - scoring=scoring, - test_prc=test_prc, - n_jobs=n_jobs, - stats_tests_to_apply=stats_tests_to_apply, - verbose=verbose, - random_state=random_state, - ) - - -class BootstrappedVolatility(TrainTestVolatility): - """ - Estimation of volatility of metrics by bootstrapping both train and test set. - - By default at every iteration the - train test split is the same. The test shows volatility of metric with regards to sampling different rows from - static train and test sets. - - Examples: - ```python - from sklearn.datasets import make_classification - from sklearn.ensemble import RandomForestClassifier - from probatus.metric_volatility import BootstrappedVolatility - X, y = make_classification(n_features=4) - clf = RandomForestClassifier() - volatility = BootstrappedVolatility(clf, iterations=10 , test_prc = 0.5) - volatility_report = volatility.fit_compute(X, y) - volatility.plot() - ``` - - """ - - def __init__( - self, - clf, - iterations=1000, - scoring="roc_auc", - train_sampling_fraction=1, - test_sampling_fraction=1, - test_prc=0.25, - n_jobs=1, - stats_tests_to_apply=None, - verbose=0, - random_state=None, - ): - """ - Initializes the class. - - Args: - clf (model object): - Binary classification model or pipeline. - - iterations (int, optional): - Number of iterations in seed bootstrapping. By default 1000. - - scoring (string, list of strings, probatus.utils.Scorer or list of probatus.utils.Scorers, optional): - Metrics for which the score is calculated. It can be either a name or list of names metric names and - needs to be aligned with predefined classification scorers names in sklearn - ([link](https://scikit-learn.org/stable/modules/model_evaluation.html)). - Another option is using probatus.utils.Scorer to define a custom metric. - - train_sampling_fraction (float, optional): - Fraction of train data sampled, if sample_train_type is not None. Default value is 1. - - test_sampling_fraction (float, optional): - Fraction of test data sampled, if sample_test_type is not None. Default value is 1. - - test_prc (float, optional): - Percentage of input data used as test. By default 0.25. - - n_jobs (int, optional): - Number of parallel executions. If -1 use all available cores. By default 1. - - stats_tests_to_apply (str or list of str, optional): - List of tests to apply, default is None. Available options: - - - `'ES'`: Epps-Singleton - - `'KS'`: Kolmogorov-Smirnov - - `'PSI'`: Population Stability Index - - `'SW'`: Shapiro-Wilk - - `'AD'`: Anderson-Darling - - Details on the available tests can be found [here](/probatus/api/stat_tests.html#available-tests). - - verbose (int, optional): - Controls verbosity of the output: - - - 0 - neither prints nor warnings are shown - - 1 - 50 - only most important warnings - - 51 - 100 - shows other warnings and prints - - above 100 - presents all prints and all warnings (including SHAP warnings). - - random_state (int, optional): - Random state set at each round of feature elimination. If it is None, the results will not be - reproducible and in random search at each iteration a different hyperparameters might be tested. For - reproducible results set it to integer. - """ - super().__init__( - clf=clf, - sample_train_test_split_seed=False, - train_sampling_type="bootstrap", - test_sampling_type="bootstrap", - iterations=iterations, - scoring=scoring, - train_sampling_fraction=train_sampling_fraction, - test_sampling_fraction=test_sampling_fraction, - test_prc=test_prc, - n_jobs=n_jobs, - stats_tests_to_apply=stats_tests_to_apply, - verbose=verbose, - random_state=random_state, - ) diff --git a/probatus/missing_values/__init__.py b/probatus/missing_values/__init__.py deleted file mode 100644 index 04a73b50..00000000 --- a/probatus/missing_values/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -from .imputation import ImputationSelector - -__all__ = ["ImputationSelector"] diff --git a/probatus/missing_values/imputation.py b/probatus/missing_values/imputation.py deleted file mode 100644 index 80e070eb..00000000 --- a/probatus/missing_values/imputation.py +++ /dev/null @@ -1,403 +0,0 @@ -# Copyright (c) 2021 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from sklearn.compose import ColumnTransformer -from sklearn.impute import SimpleImputer -from sklearn.model_selection import cross_validate -from sklearn.pipeline import Pipeline -from sklearn.preprocessing import OneHotEncoder - -from probatus.utils import BaseFitComputePlotClass, get_single_scorer, preprocess_data, preprocess_labels - - -class ImputationSelector(BaseFitComputePlotClass): - """ - Comparison of various imputation strategies that can be used for imputing missing values. - - The aim of this class is to present the model performance based on imputation - strategies and a chosen model. - For models like XGBoost & LighGBM which have capabilities to handle missing values by default - the model performance with no imputation will be shown as well. - The missing values categorical features are imputed with the value `missing` and an missing indicator is - added. - - Example: - ```python - - #Import the class - import pandas as pd - import numpy as np - import matplotlib.pyplot as plt - from probatus.missing_values.imputation import ImputationSelector - from probatus.utils.missing_helpers import generate_MCAR - from sklearn.linear_model import LogisticRegression - from sklearn.experimental import enable_iterative_imputer - from sklearn.impute import KNNImputer,SimpleImputer,IterativeImputer - from sklearn.datasets import make_classification - - # Create data with missing values. - n_features = 10 - X,y = make_classification(n_samples=1000,n_features=n_features,random_state=123,class_sep=0.3) - X = pd.DataFrame(X, columns=["f_"+str(i) for i in range(0,n_features)]) - X_missing = generate_MCAR(X,missing=0.2) - - # Create the strategies. - strategies = { - 'Simple Median Imputer' : SimpleImputer(strategy='median',add_indicator=True), - 'Simple Mean Imputer' : SimpleImputer(strategy='mean',add_indicator=True), - 'Iterative Imputer' : IterativeImputer(add_indicator=True,n_nearest_features=5, - sample_posterior=True), - 'KNN' : KNNImputer(n_neighbors=3)} - #Create a classifier. - clf = LogisticRegression() - #Create the comparison of the imputation strategies. - cmp = ImputationSelector( - clf=clf, - strategies=strategies, - cv=5, - model_na_support=False) - - cmp.fit_compute(X_missing,y) - #Plot the results. - performance_plot=cmp.plot() - - ``` - - - - """ - - def __init__( - self, - clf, - strategies, - scoring="roc_auc", - cv=5, - model_na_support=False, - n_jobs=-1, - verbose=0, - random_state=None, - ): - """ - Initialise the class. - - Args: - clf (binary classifier,sklearn.Pipeline): - A binary classification model, that will used to evaluate various imputation strategies. - - strategies (dictionary of sklearn.impute objects or any other scikit learn compatible imputer.): - Dictionary containing the sklearn.impute objects. - e.g. - strategies = {'KNN' : KNNImputer(n_neighbors=3), - 'Simple Median Imputer' : SimpleImputer(strategy='median',add_indicator=True), - 'Iterative Imputer' : IterativeImputer(add_indicator=True,n_nearest_features=5, - sample_posterior=True)} - This allows you to have fine grained control over the imputation method. - - scoring (string, list of strings, probatus.utils.Scorer or list of probatus.utils.Scorers, optional): - Metrics for which the score is calculated. It can be either a name or list of names metric names and - needs to be aligned with predefined - [classification scorers names in sklearn](https://scikit-learn.org/stable/modules/model_evaluation.html). - Another option is using probatus.utils.Scorer to define a custom metric. - - model_na_support (boolean): default False - If the classifier supports missing values by default e.g. LightGBM,XGBoost etc. - If True an default comparison `No Imputation` result will be added indicating the model performance - without any explicit imputation. - If False only the provided strategies will be used. - - n_jobs (int, optional): - Number of cores to run in parallel while fitting across folds. None means 1 unless in a - `joblib.parallel_backend` context. -1 means using all processors. - - verbose (int, optional): - Controls verbosity of the output: - - - 0 - nether prints nor warnings are shown - - 1 - 50 - only most important warnings regarding data properties are shown (excluding SHAP warnings) - - 51 - 100 - shows most important warnings, prints of the feature removal process - - above 100 - presents all prints and all warnings (including SHAP warnings). - - random_state (int, optional): - Random state set at each round of feature elimination. If it is None, the results will not be - reproducible and in random search at each iteration a different hyperparameters might be tested. For - reproducible results set it to integer. - """ # noqa - self.clf = clf - self.model_na_support = model_na_support - self.cv = cv - self.scorer = get_single_scorer(scoring) - self.strategies = strategies - self.verbose = verbose - self.n_jobs = n_jobs - self.random_state = random_state - self.fitted = False - self.report_df = pd.DataFrame([]) - - def __repr__(self): - """ - String representation. - """ - return f"Imputation comparison for {self.clf.__class__.__name__}" - - def fit(self, X, y, column_names=None): - """ - Calculates the cross validated results for various imputation strategies. - - Args: - X (pd.DataFrame): - input variables. - - y (pd.Series): - target variable. - - column_names (None, or list of str, optional): - List of feature names for the dataset. - If None, then column names from the X dataframe are used. - """ - if self.random_state is not None: - np.random.seed(self.random_state) - - # Place holder for results. - results = [] - - self.X, self.column_names = preprocess_data(X, column_names=column_names, verbose=self.verbose) - self.y = preprocess_labels(y, index=self.X.index, verbose=self.verbose) - - # Identify categorical features. - categorical_columns = X.select_dtypes(include=["category", "object"]).columns - # Identify the numeric columns.Numeric columns are all columns expect the categorical columns - numeric_columns = X.select_dtypes("number").columns - - for strategy in self.strategies: - numeric_transformer = Pipeline(steps=[("imputer", self.strategies[strategy])]) - - categorical_transformer = Pipeline( - steps=[ - ( - "imp_cat", - SimpleImputer( - strategy="constant", - fill_value="missing", - add_indicator=True, - ), - ), - ("ohe_cat", OneHotEncoder(handle_unknown="ignore")), - ] - ) - - preprocessor = ColumnTransformer( - transformers=[ - ("num", numeric_transformer, numeric_columns), - ("cat", categorical_transformer, categorical_columns), - ], - remainder="passthrough", - ) - - model_pipeline = Pipeline(steps=[("preprocessor", preprocessor), ("classifier", self.clf)]) - - temp_results = self._calculate_results(X, y, clf=model_pipeline, strategy=strategy) - - results.append(temp_results) - - # If model supports missing values by default, then calculate the scores - # on raw data without any imputation. - if self.model_na_support: - categorical_transformer = Pipeline( - steps=[ - ("ohe_cat", OneHotEncoder(handle_unknown="ignore")), - ] - ) - - preprocessor = ColumnTransformer( - transformers=[("cat", categorical_transformer, categorical_columns)], - remainder="passthrough", - ) - - model_pipeline = Pipeline(steps=[("preprocessor", preprocessor), ("classifier", self.clf)]) - - temp_results = self._calculate_results(X, y, clf=model_pipeline, strategy="No Imputation") - results.append(temp_results) - - self.report_df = pd.DataFrame(results) - # Set the index of the dataframe to the imputation methods. - self.report_df = self.report_df.set_index(self.report_df.strategy) - self.report_df.drop(columns=["strategy"], inplace=True) - self.report_df.sort_values(by="mean_test_score", inplace=True) - self.fitted = True - return self - - def _calculate_results(self, X, y, clf, strategy): - """ - Method to calculate the results for a particular imputation strategy. - - Args: - X (pd.DataFrame): - input variables. - - y (pd.Series): - target variable. - - clf (binary classifier,sklearn.Pipeline): - A binary classification model, that will used to evaluate various imputation strategies. - - strategy(string): - Name of the strategy used for imputation. - - Returns: - - temp_df(dict) : Dictionary containing the results of the evaluation. - """ - - imputation_cv_results = cross_validate( - clf, - X, - y, - scoring=self.scorer.scorer, - cv=self.cv, - n_jobs=self.n_jobs, - return_train_score=True, - ) - # Calculate the mean of the results. - imp_agg_results = {k: np.mean(v) for k, v in imputation_cv_results.items()} - imp_agg_results = {"mean_" + str(key): val for key, val in imp_agg_results.items()} - imp_agg_results["test_score_std"] = np.std(imputation_cv_results["test_score"]) - imp_agg_results["train_score_std"] = np.std(imputation_cv_results["train_score"]) - # Round off all calculations to 3 decimal places - imp_agg_results = {k: np.round(v, 3) for k, v in imp_agg_results.items()} - imp_agg_results["strategy"] = strategy - - return imp_agg_results - - def compute(self): - """ - Checks if fit() method has been run. - - and computes the DataFrame with results of imputation for each - strategy. - - Returns: - (pd.DataFrame): - DataFrame with results of imputation for each strategy. - """ - self._check_if_fitted() - return self.report_df - - def fit_compute(self, X, y, column_names=None): - """ - Calculates the cross validated results for various imputation strategies. - - Args: - X (pd.DataFrame): - input variables. - - y (pd.Series): - target variable. - - column_names (None, or list of str, optional): - List of feature names for the dataset. - If None, then column names from the X dataframe are used. - - Returns: - (pd.DataFrame): - DataFrame with results of imputation for each strategy. - - """ - self.fit(X, y, column_names=column_names) - return self.compute() - - def plot(self, show=True, **figure_kwargs): - """ - Generates plot of the performance of various imputation strategies. - - Args: - show (bool, optional): - If True, the plots are showed to the user, otherwise they are not shown. Not showing plot can be useful, - when you want to edit the returned axis, before showing it. - - **figure_kwargs: - Keyword arguments that are passed to the plt.figure, at its initialization. - - Returns: - (plt.axis): - Axis containing the performance plot. - """ - fig, ax = plt.subplots(**figure_kwargs) - - report_df = self.compute() - imp_methods = list(report_df.index) - test_performance = list(report_df["mean_test_score"]) - test_std_error = list(report_df["test_score_std"]) - train_performance = list(report_df["mean_train_score"]) - train_std_error = list(report_df["train_score_std"]) - - y = np.arange(len(imp_methods)) # the label locations - width = 0.35 # the width of the bars - - def _autolabel(rects): - """ - Label the bars of the plot. - """ - for rect in rects: - width = rect.get_width() - ax.annotate( - f"{width}", - xy=((width + 0.05 * width), rect.get_y() + rect.get_height() / 2), - xytext=(4, 0), # 4 points horizontal offset - textcoords="offset points", - ha="center", - va="bottom", - fontsize="small", - ) - - train_rect = ax.barh( - y - width / 2, - train_performance, - width, - xerr=train_std_error, - align="center", - label="CV-Train", - ) - test_rect = ax.barh( - y + width / 2, - test_performance, - width, - xerr=test_std_error, - align="center", - label="CV-Test", - ) - _autolabel(train_rect) - _autolabel(test_rect) - - ax.set_xlabel(f'{self.scorer.metric_name.replace("_"," ").upper()} Score') - ax.set_title("Imputation Techniques Comparison") - ax.set_yticks(y) - ax.set_yticklabels(imp_methods, rotation=45) - plt.margins(0.2) - plt.legend(loc="best", ncol=2) - fig.tight_layout() - - if show: - plt.show() - else: - plt.close() - return ax diff --git a/probatus/stat_tests/__init__.py b/probatus/stat_tests/__init__.py deleted file mode 100644 index 6a44af74..00000000 --- a/probatus/stat_tests/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -from .ad import ad -from .es import es -from .sw import sw -from .ks import ks -from .psi import psi -from .distribution_statistics import DistributionStatistics -from .distribution_statistics import AutoDist - -__all__ = ["ks", "psi", "ad", "es", "sw", "DistributionStatistics", "AutoDist"] diff --git a/probatus/stat_tests/ad.py b/probatus/stat_tests/ad.py deleted file mode 100644 index aaa05a82..00000000 --- a/probatus/stat_tests/ad.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -from probatus.stat_tests.utils import verbose_p_vals -from probatus.utils import NotInstalledError - -from ..utils import assure_numpy_array - -try: - from scipy import stats -except ModuleNotFoundError: - stats = NotInstalledError("scipy", "extras") - - -@verbose_p_vals -def ad(d1, d2, verbose=False): - """ - Calculates the Anderson-Darling test statistic on 2 distributions. - - Can be used on continuous or discrete distributions. - - Any binning/bucketing of the distributions/samples should be done before passing them to this function. - - Advantages: - - - Unlike the KS, the AD (like the ES) can be used on both continuous & discrete distributions. - - Works well even when the sample has fewer than 25 observations. - - More powerful than KS, especially for differences in the tails of distributions. - - References: - - - [Wikipedia article about the Anderson-Darling test](https://en.wikipedia.org/wiki/Anderson%E2%80%93Darling_test) - - [SciPy documentation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.anderson_ksamp.html) - - Args: - d1 (np.array or pandas.Series): First sample. - - d2 (np.array or pandas.Series): Second sample. - - verbose (bool): If True, useful interpretation info is printed to stdout. - - Returns: - float: Anderson-Darling test statistic. - float: p-value of rejecting the null hypothesis (that the two distributions are identical). - """ - d1 = assure_numpy_array(d1) - d2 = assure_numpy_array(d2) - - ad, critical_values, pvalue = stats.anderson_ksamp([d1, d2]) - - return ad, pvalue diff --git a/probatus/stat_tests/distribution_statistics.py b/probatus/stat_tests/distribution_statistics.py deleted file mode 100644 index 6de31d48..00000000 --- a/probatus/stat_tests/distribution_statistics.py +++ /dev/null @@ -1,424 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import itertools -import warnings - -import numpy as np -import pandas as pd -from tqdm import tqdm - -from probatus.binning import AgglomerativeBucketer, QuantileBucketer, SimpleBucketer -from probatus.stat_tests import ad, es, ks, psi, sw -from probatus.utils.arrayfuncs import check_numeric_dtypes - - -class DistributionStatistics: - """ - Wrapper that applies a statistical test to compare two distributions. - - Details on the available tests can be found [here](/probatus/api/stat_tests.html#available-tests). - - For some tests, default data binning strategies are also provided. - - Example: - ```python - import numpy as np - import pandas as pd - from probatus.stat_tests import DistributionStatistics - - d1 = np.histogram(np.random.normal(size=1000), 10)[0] - d2 = np.histogram(np.random.normal(size=1000), 10)[0] - myTest = DistributionStatistics('KS', bin_count=10) - test_statistic, p_value = myTest.compute(d1, d2, verbose=True) - ``` - """ - - binning_strategy_dict = { - "simplebucketer": SimpleBucketer, - "agglomerativebucketer": AgglomerativeBucketer, - "quantilebucketer": QuantileBucketer, - None: None, - } - statistical_test_dict = { - "ES": { - "func": es, - "name": "Epps-Singleton", - "default_binning": None, - }, - "KS": { - "func": ks, - "name": "Kolmogorov-Smirnov", - "default_binning": None, - }, - "AD": { - "func": ad, - "name": "Anderson-Darling TS", - "default_binning": None, - }, - "SW": { - "func": sw, - "name": "Shapiro-Wilk based difference", - "default_binning": None, - }, - "PSI": { - "func": psi, - "name": "Population Stability Index", - "default_binning": "quantilebucketer", - }, - } - - def __init__(self, statistical_test, binning_strategy="default", bin_count=10): - """ - Initializes the class. - - Args: - statistical_test (str): Statistical test to apply. Available tests: - - - `'ES'`: Epps-Singleton - - `'KS'`: Kolmogorov-Smirnov - - `'PSI'`: Population Stability Index - - `'SW'`: Shapiro-Wilk - - `'AD'`: Anderson-Darling - - Details on the available tests can be found [here](/probatus/api/stat_tests.html#available-tests) - - binning_strategy (string, optional): - Binning strategy to apply, binning strategies implemented: - - - `'simplebucketer'`: equally spaced bins, - - `'agglomerativebucketer'`: binning by applying the Scikit-learn implementation of Agglomerative - Clustering, - - `'quantilebucketer'`: bins with equal number of elements, - - `'default'`: applies a default binning for a given stats_test. For all tests apart from PSI, no - binning (None) is used. For PSI by default quantilebucketer is used, - - `None`: no binning is applied. The test is computed based on original distribution. - - bin_count (int, optional): In case binning_strategy is not None, specify the number of bins to be used by - the binning strategy. By default 10 bins are used. - """ - self.statistical_test = statistical_test.upper() - self.binning_strategy = binning_strategy - self.bin_count = bin_count - self.fitted = False - - # Initialize the statistical test - if self.statistical_test not in self.statistical_test_dict: - raise NotImplementedError(f"The statistical test should be one of {self.statistical_test_dict.keys()}") - else: - self.statistical_test_name = self.statistical_test_dict[self.statistical_test]["name"] - self._statistical_test_function = self.statistical_test_dict[self.statistical_test]["func"] - - # Initialize the binning strategy - if self.binning_strategy: - self.binning_strategy = self.binning_strategy.lower() - if self.binning_strategy == "default": - self.binning_strategy = self.statistical_test_dict[self.statistical_test]["default_binning"] - if self.binning_strategy not in self.binning_strategy_dict: - raise NotImplementedError( - f"The binning strategy should be one of {list(self.binning_strategy_dict.keys())}" - ) - else: - binner = self.binning_strategy_dict[self.binning_strategy] - if binner is not None: - self.binner = binner(bin_count=self.bin_count) - - def __repr__(self): - """ - String representation. - """ - repr_ = f"DistributionStatistics object\n\tstatistical_test: {self.statistical_test}" - if self.binning_strategy: - repr_ += f"\n\tbinning_strategy: {self.binning_strategy}\n\tbin_count: {self.bin_count}" - else: - repr_ += "\n\tNo binning applied" - if self.fitted: - repr_ += f"\nResults\n\tvalue {self.statistical_test}-statistic: {self.statistic}" - if hasattr(self, "p_value"): - repr_ += f"\n\tp-value: {self.p_value}" - return repr_ - - def compute(self, d1, d2, verbose=False): - """ - Apply the statistical test and compute statistic value and p-value. - - Args: - d1 (np.array or pandas.DataFrame): - distribution 1. - - d2 (np.array or pandas.DataFrame): - distribution 2. - - verbose (bool, optional): - Flag indicating whether prints should be shown. - - Returns: - float: Statistic value - float: p_value. For PSI test, only the statistic value is returned - """ - check_numeric_dtypes(d1) - check_numeric_dtypes(d2) - - # Bin the data - if self.binning_strategy: - self.binner.fit(d1) - d1_preprocessed = self.binner.compute(d1) - d2_preprocessed = self.binner.compute(d2) - else: - d1_preprocessed, d2_preprocessed = d1, d2 - - # Perform the statistical test - res = self._statistical_test_function(d1_preprocessed, d2_preprocessed, verbose=verbose) - self.fitted = True - - # Check form of results and return - if type(res) == tuple: - self.statistic, self.p_value = res - return self.statistic, self.p_value - else: - self.statistic = res - return self.statistic - - -class AutoDist: - """Apply stat tests and binning strategies. - - Class to automatically apply all implemented statistical distribution tests and binning strategies - to (a selection of) features in two dataframes. - - Details on the available tests can be found [here](/probatus/api/stat_tests.html#available-tests). - - Example: - ```python - import numpy as np - import pandas as pd - from probatus.stat_tests import AutoDist - - df1 = pd.DataFrame(np.random.normal(size=(1000, 2)), columns=['feat_0', 'feat_1']) - df2 = pd.DataFrame(np.random.normal(size=(1000, 2)), columns=['feat_0', 'feat_1']) - myAutoDist = AutoDist(statistical_tests=["KS", "PSI"], binning_strategies='simplebucketer', bin_count=10) - myAutoDist.compute(df1, df2, column_names=df1.columns) - ``` - - - """ - - def __init__(self, statistical_tests="all", binning_strategies="default", bin_count=10): - """ - Initializes the class. - - Args: - statistical_tests (str or list of str, optional): Test or list of tests to apply. - Set to `'all'` to apply all the available test. Available tests: - - - `'ES'`: Epps-Singleton - - `'KS'`: Kolmogorov-Smirnov - - `'PSI'`: Population Stability Index - - `'SW'`: Shapiro-Wilk - - `'AD'`: Anderson-Darling - - Details on the available tests can be found [here](/probatus/api/stat_tests.html#available-tests). - - binning_strategies (str, optional): Binning strategies to apply for each test, either list of tests names, - 'all' or 'default'. Binning strategies that can be chosen: - - - `'SimpleBucketer'`: equally spaced bins, - - `'AgglomerativeBucketer'`: binning by applying the Scikit-learn implementation of Agglomerative - Clustering, - - `'QuantileBucketer'`: bins with equal number of elements, - - `None`: no binning is applied. Note that not all statistical tests will be performed since some of - them require binning strategies. - - `'default'`: applies a default binning for a given stats_test. For all tests apart from PSI, no - binning (None) is used. For PSI by default quantilebucketer is used. - - `'all'`: each binning strategy is used for each statistical test - - bin_count (integer, None or list of integers, optional): - bin_count value(s) to be used, note that None can only be used when no bucketing strategy is applied. - """ - self.fitted = False - - # Initialize statistical tests to be performed - if statistical_tests == "all": - self.statistical_tests = list(DistributionStatistics.statistical_test_dict.keys()) - elif isinstance(statistical_tests, str): - self.statistical_tests = [statistical_tests] - else: - self.statistical_tests = statistical_tests - - # Initialize binning strategies to be used - if binning_strategies == "all": - self.binning_strategies = list(DistributionStatistics.binning_strategy_dict.keys()) - elif isinstance(binning_strategies, str): - self.binning_strategies = [binning_strategies] - elif binning_strategies is None: - self.binning_strategies = [None] - else: - self.binning_strategies = binning_strategies - if not isinstance(bin_count, list): - self.bin_count = [bin_count] - else: - self.bin_count = bin_count - - def __repr__(self): - """ - String representation. - """ - repr_ = "AutoDist object" - if not self.fitted: - repr_ += "\n\tAutoDist not fitted" - if self.fitted: - repr_ += "\n\tAutoDist fitted" - repr_ += f"\n\tstatistical_tests: {self.statistical_tests}" - repr_ += f"\n\tbinning_strategies: {self.binning_strategies}" - repr_ += f"\n\tbin_count: {self.bin_count}" - return repr_ - - def compute( - self, - df1, - df2, - column_names=None, - return_failed_tests=True, - suppress_warnings=False, - ): - """ - Fit the AutoDist object to data; i.e. apply the statistical tests and binning strategies. - - Args: - - df1 (pandas.DataFrame): - DataFrame 1 for distribution comparison with DataFrame 2. - - df2 (pandas.DataFrame): - DataFrame 2 for distribution comparison with DataFrame 1. - - column_names (list of str, optional): - list of columns in df1 and df2 that should be compared. If None, all column names will be compared. - - return_failed_tests (bool, optional): - remove tests in result that did not succeed. - - suppress_warnings (bool, optional): - whether to suppress warnings during the fit process. - - Returns: - pandas.DataFrame: DataFrame with results of the performed statistical tests and binning strategies. - - """ - if column_names is None: - column_names = df1.columns.to_list() - if len(set(column_names) - set(df2.columns)): - raise Exception("column_names was set to None but columns in provided dataframes are different") - # Check if all columns in column_names are in df1 and df2 - elif len(set(column_names) - set(df1.columns)) or len(set(column_names) - set(df2.columns)): - raise Exception("Not all columns in `column_names` are in the provided dataframes") - - # Calculate statistics and p-values for all combinations - result_all = [] - for col in column_names: - # Issue a warning if missing values are present in one of the two columns. These observations are removed - # in the calculations. - if np.sum(df1[col].isna()) + np.sum(df2[col].isna()): - warnings.warn(f"Missing values in column {col} have been removed") - - # Remove the missing values. - feature_df1 = df1[col].dropna() - feature_df2 = df2[col].dropna() - - for stat_test, bin_strat, bins in tqdm( - list( - itertools.product( - self.statistical_tests, - self.binning_strategies, - self.bin_count, - ) - ) - ): - if self.binning_strategies == ["default"]: - bin_strat = DistributionStatistics.statistical_test_dict[stat_test]["default_binning"] - - dist = DistributionStatistics( - statistical_test=stat_test, - binning_strategy=bin_strat, - bin_count=bins, - ) - try: - if suppress_warnings: - warnings.filterwarnings("ignore") - _ = dist.compute(feature_df1, feature_df2) - if suppress_warnings: - warnings.filterwarnings("default") - statistic = dist.statistic - p_value = dist.p_value - except Exception: - statistic, p_value = "an error occurred", None - pass - - # Append result to results list - result_ = { - "column": col, - "statistical_test": stat_test, - "binning_strategy": bin_strat, - "bin_count": bins, - "statistic": statistic, - "p_value": p_value, - } - - result_all.append(result_) - - result_all = pd.DataFrame(result_all) - - if not return_failed_tests: - result_all = result_all[result_all["statistic"] != "an error occurred"] - self.fitted = True - self._result = result_all[ - [ - "column", - "statistical_test", - "binning_strategy", - "bin_count", - "statistic", - "p_value", - ] - ] - self._result["bin_count"] = self._result["bin_count"].astype(int) - self._result.loc[self._result["binning_strategy"].isnull(), "bin_count"] = 0 - self._result.loc[self._result["binning_strategy"].isnull(), "binning_strategy"] = "no_bucketing" - - # Remove duplicates that appear if multiple bin numbers are passed, and binning strategy None - - self._result = self._result.drop_duplicates( - subset=["column", "statistical_test", "binning_strategy", "bin_count"], - keep="first", - ) - - # create pivot table as final output - self.result = pd.pivot_table( - self._result, - values=["statistic", "p_value"], - index="column", - columns=["statistical_test", "binning_strategy", "bin_count"], - aggfunc="sum", - ) - - # flatten multi-index - self.result.columns = ["_".join([str(x) for x in line]) for line in self.result.columns.values] - self.result.reset_index(inplace=True) - return self.result diff --git a/probatus/stat_tests/es.py b/probatus/stat_tests/es.py deleted file mode 100644 index a3a433b4..00000000 --- a/probatus/stat_tests/es.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -from probatus.utils import NotInstalledError - -try: - from scipy import stats -except ModuleNotFoundError: - stats = NotInstalledError("scipy", "extras") - -from probatus.stat_tests.utils import verbose_p_vals - -from ..utils import assure_numpy_array - - -@verbose_p_vals -def es(d1, d2, verbose=False): - """ - Calculates the Epps-Singleton test statistic on 2 distributions. - - Can be used on continuous or discrete distributions. - Any binning/bucketing of the distributions/samples should be done before passing them to this - function. - - Whereas KS relies on the empirical distribution function, ES is based on the empirical characteristic function - (Epps & Singleton 1986, Goerg & Kaiser 2009). - - Advantages: - - - Unlike the KS, the ES can be used on both continuous & discrete distributions. - - - ES has higher power (vs KS) in many examples. - - Disadvantages: - - - Not recommended for fewer than 25 observations. Instead, use the Anderson-Darling TS. (However, ES can still be - used for small samples. A correction factor is applied so that the asymptotic TS distribution more closely follows - the chi-squared distribution, such that p-values can be computed.) - - - References: - - - [SciPy documentation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.epps_singleton_2samp.html) - - Args: - d1 (np.array or pandas.Series): First sample. - - d2 (np.array or pandas.Series): Second sample. - - verbose (bool): If True, useful interpretation info is printed to stdout. - - Returns: - float: Epps-Singleton test statistic - float: p-value of rejecting the null hypothesis (that the two distributions are identical) - """ - d1 = assure_numpy_array(d1) - d2 = assure_numpy_array(d2) - - es, pvalue = stats.epps_singleton_2samp(d1, d2) - - return es, pvalue diff --git a/probatus/stat_tests/ks.py b/probatus/stat_tests/ks.py deleted file mode 100644 index 175951ab..00000000 --- a/probatus/stat_tests/ks.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -from probatus.utils import NotInstalledError - -try: - from scipy import stats -except ModuleNotFoundError: - stats = NotInstalledError("scipy", "extras") - -from probatus.stat_tests.utils import verbose_p_vals - -from ..utils import assure_numpy_array - - -@verbose_p_vals -def ks(d1, d2, verbose=False): - """ - Calculates the Kolmogorov-Smirnov test statistic on 2 samples. - - Any binning/bucketing of the distributions/samples should be done before passing them to this function. - - References: - - - [Wikipedia article about Kolmogorov-Smirnov test](https://en.wikipedia.org/wiki/Kolmogorov%E2%80%93Smirnov_test) - - [SciPy documentation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.ks_2samp.html) - - Args: - d1 (np.ndarray or pandas.Series): First sample. - - d2 (np.ndarray or pandas.Series): Second sample. - - verbose (bool): If True, useful interpretation info is printed to stdout. - - Returns: - float: Kolmogorov-Smirnov test statistic. - float: p-value of rejecting the null hypothesis (that the two distributions are identical). - """ - # Perform data checks - d1 = assure_numpy_array(d1) - d2 = assure_numpy_array(d2) - - # Perform statistical tests - ks, pvalue = stats.ks_2samp(d1, d2) - - return ks, pvalue diff --git a/probatus/stat_tests/psi.py b/probatus/stat_tests/psi.py deleted file mode 100644 index 41f55e2b..00000000 --- a/probatus/stat_tests/psi.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import warnings - -import numpy as np - -from probatus.utils import NotInstalledError - -try: - from scipy import stats -except ModuleNotFoundError: - stats = NotInstalledError("scipy", "extras") - -from ..utils import assure_numpy_array - - -def psi(d1, d2, verbose=False): - """ - Calculates the Population Stability Index. - - A simple statistical test that quantifies the similarity of two distributions. - Commonly used in the banking / risk modeling industry. - Only works on categorical data or bucketed numerical data. - Distributions must be binned/bucketed before passing them to this function. - Bin boundaries should be the same for both distributions. - Distributions must have the same number of buckets. - Note that the PSI varies with number of buckets chosen (typically 10-20 bins are used). - Quantile bucketing is typically recommended. - - References: - - - [Statistical Properties of Population Stability Index](https://scholarworks.wmich.edu/cgi/viewcontent.cgi?article=4249&context=dissertations) - - - Args: - d1 (np.ndarray or pandas.Series): First distribution ("expected"). - - d2 (np.ndarray or pandas.Series): Second distribution ("actual"). - - verbose (bool): If True, useful interpretation info is printed to stdout. - - - Returns: - float: Measure of the similarity between d1 & d2. (range 0-inf, with 0 indicating identical - distributions and > 0.25 indicating significantly different distributions) - float: p-value for rejecting null hypothesis (that the two distributions are identical) - """ # noqa - # Perform data checks - d1 = assure_numpy_array(d1) - d2 = assure_numpy_array(d2) - - if len(d1) < 10: - warnings.warn("PSI is not well-behaved when using less than 10 bins.") - if len(d1) > 20: - warnings.warn("PSI is not well-behaved when using more than 20 bins.") - if len(d1) != len(d2): - raise ValueError("Distributions do not have the same number of bins.") - - # Number of bins/buckets - b = len(d1) - - # Calculate the number of samples in each distribution - n = d1.sum() - m = d2.sum() - - # Calculate the ratio of samples in each bin - expected_ratio = d1 / n - actual_ratio = d2 / m - - # Necessary to avoid divide by zero and ln(0). Should have minor impact on PSI value. - has_empty_bucket = False - for i in range(b): - if expected_ratio[i] == 0: - expected_ratio[i] = 0.0001 - has_empty_bucket = True - - if actual_ratio[i] == 0: - actual_ratio[i] = 0.0001 - has_empty_bucket = True - - if has_empty_bucket: - warnings.warn( - "PSI: Some of the buckets have zero counts. In theory this situation would mean PSI=Inf due to " - "division by 0. However, we artificially modified the count of samples in these bins to a small " - "number. This may cause that the PSI value for this feature is over-estimated (larger). " - "Decreasing the number of buckets may also help avoid buckets with zero counts." - ) - - # Calculate the PSI value - psi_value = np.sum((actual_ratio - expected_ratio) * np.log(actual_ratio / expected_ratio)) - - # Print the evaluation of statistical hypotheses - if verbose: - print("\nPSI =", psi_value) - - print("\nPSI: Critical values defined according to de facto industry standard:") - if psi_value <= 0.1: - print("PSI <= 0.10: No significant distribution change.") - elif 0.1 < psi_value <= 0.25: - print("PSI <= 0.25: Small distribution change; may require investigation.") - elif psi_value > 0.25: - print("PSI > 0.25: Significant distribution change; investigate.") - - # Calculate the critical values and - alpha = [0.95, 0.99, 0.999] - z_alpha = stats.norm.ppf(alpha) - psi_critvals = ((1 / n) + (1 / m)) * (b - 1) + z_alpha * ((1 / n) + (1 / m)) * np.sqrt(2 * (b - 1)) - print("\nPSI: Critical values defined according to Yurdakul (2018):") - if psi_value > psi_critvals[2]: - print("99.9% confident distributions have changed.") - elif psi_value > psi_critvals[1]: - print("99% confident distributions have changed.") - elif psi_value > psi_critvals[0]: - print("95% confident distributions have changed.") - elif psi_value < psi_critvals[0]: - print("No significant distribution change.") - - # Calculate p-value - z = (psi_value / ((1 / n) + (1 / m)) - (b - 1)) / np.sqrt(2 * (b - 1)) - p_value = 1 - stats.norm.cdf(z) - - return psi_value, p_value diff --git a/probatus/stat_tests/sw.py b/probatus/stat_tests/sw.py deleted file mode 100644 index bd27d44c..00000000 --- a/probatus/stat_tests/sw.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import random - -import pandas as pd - -from probatus.utils import NotInstalledError - -from ..utils import assure_numpy_array - -try: - from scipy import stats -except ModuleNotFoundError: - stats = NotInstalledError("scipy", "extras") - - -def sw(d1, d2, verbose=False): - """ - Calculates the Shapiro-Wilk test statistic on 2 distributions. - - This examines whether deviation from normality of two distributions are significantly different. - - References: - - - [Wikipedia article about the Shapiro-Wilk test](https://en.wikipedia.org/wiki/Shapiro%E2%80%93Wilk_test) - - [SciPy documentation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.shapiro.html) - - - Args: - d1 (np.ndarray or pandas.Series): First sample. - - d2 (np.ndarray or pandas.Series): Second sample. - - verbose (bool): If True, useful interpretation info is printed to stdout. - - Returns: - float: Shapiro-Wilk test statistic - float: p-value of rejecting the null hypothesis (that the two distributions are identical) - """ - d1 = assure_numpy_array(d1) - d2 = assure_numpy_array(d2) - - if len(d1) > 5000: - d1 = pd.Series(random.choices(d1, k=5000)) - if len(d2) > 5000: - d2 = pd.Series(random.choices(d2, k=5000)) - - delta = stats.shapiro(d1)[0] - stats.shapiro(d2)[0] - - d1 = pd.Series(d1) - d2 = pd.Series(d2) - - MOT = pd.concat([d1, d2]) - n1 = d1.shape[0] - n2 = d2.shape[0] - - def ran_delta(n1, n2): - take_ran = lambda n: random.sample(range(MOT.shape[0]), n) - ran_1 = MOT.iloc[take_ran(n1),] - ran_2 = MOT.iloc[take_ran(n2),] - delta_ran = stats.shapiro(ran_1)[0] - stats.shapiro(ran_2)[0] - return delta_ran - - collect = [ran_delta(n1, n2) for a in range(100)] - collect = pd.Series(list(collect)) - delta_p_value = 1 - stats.percentileofscore(collect, delta) / 100 - - quants = [0.025, 0.975] - sig_vals = list(collect.quantile(quants)) - - if verbose: - if delta < sig_vals[0] or delta > sig_vals[1]: - print("\nShapiro_Difference | Null hypothesis : REJECTED.") - print("\nDelta is outside 95% CI -> Distributions very different.") - else: - print("\nShapiro_Difference | Null hypothesis : NOT REJECTED.") - print("\nDelta is inside 95% CI -> Distributions are not different.") - - return delta, delta_p_value diff --git a/probatus/stat_tests/utils.py b/probatus/stat_tests/utils.py deleted file mode 100644 index bf5b307b..00000000 --- a/probatus/stat_tests/utils.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) 2020 ING Bank N.V. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import functools - - -def verbose_p_vals(func): - """ - Decorator to enable verbose printing of p-values. - """ - - @functools.wraps(func) - def wrapper_verbose_p_vals(*args, **kwargs): - test_name = func.__name__.upper() - - stat, pvalue = func(*args, **kwargs) - - if "verbose" in kwargs and kwargs["verbose"] is True: - print(f"\n{test_name}: pvalue =", pvalue) - if pvalue < 0.01: - print( - "\n{}: Null hypothesis rejected with 99% confidence. Distributions very different.".format( - test_name - ) - ) - elif pvalue < 0.05: - print(f"\n{test_name}: Null hypothesis rejected with 95% confidence. Distributions different.") - else: - print( - "\n{}: Null hypothesis cannot be rejected. Distributions not statistically different.".format( - test_name - ) - ) - - return stat, pvalue - - return wrapper_verbose_p_vals diff --git a/pyproject.toml b/pyproject.toml index 09e58e11..7c4cee05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "probatus" -version = "2.1.1" +version = "3.0.0" requires-python= ">=3.8" description = "Validation of binary classifiers and data used to develop them" readme = { file = "README.md", content-type = "text/markdown" } @@ -44,14 +44,9 @@ Repository = "https://github.com/ing-bank/probatus.git" Changelog = "https://github.com/ing-bank/probatus/blob/main/CHANGELOG.md" [project.optional-dependencies] -all = [ - "lightgbm>=3.3.0", - # https://github.com/catboost/catboost/issues/2371 - "catboost<1.2 ; python_version == '3.8'", - "catboost>=1.1 ; python_version != '3.8'", - "xgboost>=1.5.0", - "scipy>=1.4.0", - # Dev dependencies + + +dev = [ "black>=19.10b0", "pre-commit>=2.5.0", "mypy>=0.770", @@ -66,21 +61,25 @@ all = [ "pre-commit>=2.7.1", "isort>=5.12.0", "codespell>=2.2.4", - "ruff>=0.0.272", - # Doc dependencies - "mkdocs-material>=6.1.0", - "mkdocs-git-revision-date-localized-plugin>=0.7.2", - "mkdocs-git-authors-plugin>=0.3.2", - "mkdocs-table-reader-plugin>=0.4.1", - "mkdocs-enumerate-headings-plugin>=0.4.3", - "mkdocs-awesome-pages-plugin>=2.4.0", - "mkdocs-minify-plugin>=0.3.0", - "mknotebooks>=0.6.2", - "mkdocstrings>=0.13.6", - "mkdocs-print-site-plugin>=0.8.2", - "mkdocs-markdownextradata-plugin>=0.1.9", - "mkdocstrings-python>=1.1.2", + "ruff>=0.2.2", +] +docs = [ + "mkdocs>=1.5.3", + "mkdocs-jupyter>=0.24.3", + "mkdocs-material>=9.5.13", + "mkdocstrings>=0.24.1", + "mkdocstrings-python>=1.8.0", ] +extras = [ + "lightgbm>=3.3.0", + # https://github.com/catboost/catboost/issues/2371 + "catboost<1.2 ; python_version == '3.8'", + "catboost>=1.1 ; python_version != '3.8'", + "xgboost>=1.5.0", + "scipy>=1.4.0", +] +# Separating these allow for more install flexibility. +all = ["probatus[dev,docs,extras]"] [tool.setuptools.packages.find] exclude = ["tests", "notebooks", "docs"] @@ -99,6 +98,10 @@ pretty = true [tool.ruff] line-length = 120 +extend-exclude = ["docs", "mkdocs.yml", ".github", "*md", "LICENCE", ".pre-commit-config.yaml", ".gitignore"] +force-exclude = true + +[tool.ruff.lint] # D100 requires all Python files (modules) to have a "public" docstring even if all functions within have a docstring. # D104 requires __init__ files to have a docstring # D202 No blank lines allowed after function docstring @@ -111,10 +114,8 @@ line-length = 120 # E731 do not assign a lambda expression, use a def # W293 blank line contains whitespace ignore = ["D100", "D104", "D202", "D212", "D200", "E203", "E731", "W293", "D412", "D417", "D411", "RUF100"] -extend-exclude = ["docs", "mkdocs.yml", ".github", "*md", "LICENCE", ".pre-commit-config.yaml", ".gitignore"] -force-exclude = true -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "google" [tool.isort] diff --git a/tests/binning/__init__.py b/tests/binning/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/binning/test_binning.py b/tests/binning/test_binning.py deleted file mode 100644 index a9ec56f3..00000000 --- a/tests/binning/test_binning.py +++ /dev/null @@ -1,336 +0,0 @@ -import numpy as np -import pytest -from sklearn.exceptions import NotFittedError - -from probatus.binning import AgglomerativeBucketer, Bucketer, QuantileBucketer, SimpleBucketer, TreeBucketer - - -@pytest.mark.filterwarnings("ignore:") -def test_deprecations(): - """ - Test. - """ - x = [1, 2, 1] - bins = 3 - myBucketer = SimpleBucketer(bin_count=bins) - myBucketer.fit(x) - with pytest.deprecated_call(): - myBucketer.counts - - with pytest.deprecated_call(): - myBucketer.boundaries - - -def test_simple_bins(): - """ - Test. - """ - x = [1, 2, 1] - bins = 3 - myBucketer = SimpleBucketer(bin_count=bins) - with pytest.raises(NotFittedError): - myBucketer.compute([1, 2]) - - myBucketer.fit(x) - assert len(myBucketer.counts_) == bins - assert np.array_equal(myBucketer.counts_, np.array([2, 0, 1])) - assert len(myBucketer.boundaries_) == bins + 1 - np.testing.assert_array_almost_equal(myBucketer.boundaries_, np.array([-np.inf, 1.33333333, 1.66666667, np.inf])) - # test static method - counts, boundaries = SimpleBucketer(bin_count=bins).simple_bins(x, bins) - assert np.array_equal(myBucketer.counts_, counts) - np.testing.assert_array_almost_equal(myBucketer.boundaries_, boundaries) - assert repr(myBucketer).startswith("SimpleBucketer") - - -def test_quantile_bins(): - """ - Test. - """ - bins = 4 - random_state = np.random.RandomState(0) - x = random_state.normal(0, 1, size=1000) - myBucketer = QuantileBucketer(bin_count=bins) - with pytest.raises(NotFittedError): - myBucketer.compute([1, 2]) - myBucketer.fit(x) - assert len(myBucketer.counts_) == bins - assert np.array_equal(myBucketer.counts_, np.array([250, 250, 250, 250])) - assert len(myBucketer.boundaries_) == bins + 1 - np.testing.assert_array_almost_equal( - myBucketer.boundaries_, np.array([-np.inf, -0.7, -0.1, 0.6, np.inf]), decimal=1 - ) - # test static method - counts, boundaries = QuantileBucketer(bin_count=bins).quantile_bins(x, bins) - assert np.array_equal(myBucketer.counts_, counts) - np.testing.assert_array_almost_equal(myBucketer.boundaries_, boundaries) - # test inf edges - counts, boundaries = QuantileBucketer(bin_count=bins).quantile_bins(x, bins, inf_edges=True) - assert boundaries[0] == -np.inf - assert boundaries[-1] == np.inf - assert repr(myBucketer).startswith("QuantileBucketer") - - -def test_agglomerative_clustering_new(): - """ - Test. - """ - - x = [0.5, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4.5] - bins = 4 - myBucketer = AgglomerativeBucketer(bin_count=bins) - with pytest.raises(NotFittedError): - myBucketer.compute([1, 2]) - myBucketer.fit(x) - assert len(myBucketer.counts_) == bins - print(myBucketer.counts_) - assert np.array_equal(myBucketer.counts_, np.array([4, 2, 5, 3])) - assert len(myBucketer.boundaries_) == bins + 1 - np.testing.assert_array_almost_equal(myBucketer.boundaries_, np.array([-np.inf, 1.5, 2.5, 3.5, np.inf]), decimal=2) - # test static method - counts, boundaries = AgglomerativeBucketer(bin_count=bins).agglomerative_clustering_binning(x, bins) - assert np.array_equal(myBucketer.counts_, counts) - np.testing.assert_array_almost_equal(myBucketer.boundaries_, boundaries) - assert repr(myBucketer).startswith("AgglomerativeBucketer") - - -def test_compute(): - """ - Test. - """ - x = np.arange(10) - bins = 5 - myBucketer = QuantileBucketer(bins) - x_new = x - with pytest.raises(NotFittedError): - assert myBucketer.compute(x_new) - myBucketer.fit(x) - assert len(myBucketer.compute(x_new)) == bins - np.testing.assert_array_equal(myBucketer.counts_, myBucketer.compute(x_new)) - np.testing.assert_array_equal(myBucketer.counts_, myBucketer.fit_compute(x_new)) - x_new = x + 100 - np.testing.assert_array_equal(np.array([0, 0, 0, 0, 10]), myBucketer.compute(x_new)) - x_new = x - 100 - np.testing.assert_array_equal(np.array([10, 0, 0, 0, 0]), myBucketer.compute(x_new)) - x_new = [1, 1, 1, 4, 4, 7] - np.testing.assert_array_equal(np.array([3, 0, 2, 1, 0]), myBucketer.compute(x_new)) - - -def test_quantile_with_unique_values(): - """ - Test. - """ - np.random.seed(42) - dist_0_1 = np.random.uniform(size=20) - dist_peak_at_0 = np.zeros(shape=20) - - skewed_dist = np.hstack((dist_0_1, dist_peak_at_0)) - actual_out = QuantileBucketer(10).quantile_bins(skewed_dist, 10) - - expected_out = ( - np.array([20, 4, 4, 4, 4, 4]), - np.array([0.0, 0.01894458, 0.23632033, 0.42214475, 0.60977678, 0.67440958, 0.99940487]), - ) - - assert (actual_out[0] == expected_out[0]).all() - - -def test_tree_bucketer(): - """ - Test. - """ - x = np.array( - [ - 0.0, - 0.2, - 0.4, - 0.6, - 0.8, - 1.0, - 1.2, - 1.4, - 1.6, - 1.8, - 2.0, - 2.2, - 2.4, - 2.6, - 2.8, - 3.0, - 3.2, - 3.4, - 3.6, - 3.8, - 4.0, - 4.2, - 4.4, - 4.6, - 4.8, - 5.0, - 5.2, - 5.4, - 5.6, - 5.8, - 6.0, - 6.2, - 6.4, - 6.6, - 6.8, - 7.0, - 7.2, - 7.4, - 7.6, - 7.8, - 8.0, - 8.2, - 8.4, - 8.6, - 8.8, - 9.0, - 9.2, - 9.4, - 9.6, - 9.8, - ] - ) - - y = np.array( - [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 1, - 0, - 0, - 1, - 0, - 1, - 1, - 0, - 0, - 0, - 1, - 1, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - ] - ) - - myTreeBucketer = TreeBucketer(inf_edges=False, max_depth=3, min_samples_leaf=10, random_state=42) - - with pytest.raises(NotFittedError): - myTreeBucketer.compute([1, 2]) - - myTreeBucketer.fit(x, y) - - assert all(myTreeBucketer.counts_ == np.array([21, 15, 14])) - assert myTreeBucketer.bin_count == 3 - assert all(myTreeBucketer.boundaries_ - np.array([0.0, 4.1, 7.1, 9.8]) < 0.01) - - # If infinite edges is False, it must get the edges of the x array - assert myTreeBucketer.boundaries_[0] == 0 - assert myTreeBucketer.boundaries_[-1] == 9.8 - - myTreeBucketer = TreeBucketer(inf_edges=True, max_depth=3, min_samples_leaf=10, random_state=42) - - myTreeBucketer.fit(x, y) - # check that the infinite edges is True, then edges must be infinite - assert myTreeBucketer.boundaries_[0] == -np.inf - assert myTreeBucketer.boundaries_[-1] == +np.inf - - -def test_tree_bucketer_dependence(): - """ - Test. - """ - x = np.arange(0, 10, 0.01) - y = [1 if z < 0.5 else 0 for z in np.random.uniform(size=x.shape[0])] - - # Test number of leaves is always within the expected ranges - myTreeBucketer = TreeBucketer(inf_edges=False, max_depth=3, min_samples_leaf=10, random_state=42).fit(x, y) - assert myTreeBucketer.bin_count <= np.power(2, myTreeBucketer.tree.max_depth) - - # Test number of leaves is always within the expected ranges - myTreeBucketer = TreeBucketer(inf_edges=False, max_depth=6, min_samples_leaf=1, random_state=42).fit(x, y) - assert myTreeBucketer.bin_count <= np.power(2, myTreeBucketer.tree.max_depth) - - # Test that the counts per bin never drop below min_samples_leaf - myTreeBucketer = TreeBucketer(inf_edges=False, max_depth=6, min_samples_leaf=100, random_state=42).fit(x, y) - assert all([x >= myTreeBucketer.tree.min_samples_leaf for x in myTreeBucketer.counts_]) - - myTreeBucketer = TreeBucketer(inf_edges=False, max_depth=6, min_samples_leaf=200, random_state=42).fit(x, y) - assert all([x >= myTreeBucketer.tree.min_samples_leaf for x in myTreeBucketer.counts_]) - - # Test that if the leaf is set to the number of entries,it raises an Error - myTreeBucketer = TreeBucketer(inf_edges=False, max_depth=6, min_samples_leaf=x.shape[0], random_state=42) - - with pytest.raises(ValueError): - assert myTreeBucketer.fit(x, y) - - # Test that if the leaf is set to the number of entries-1, it returns only one bin - myTreeBucketer = TreeBucketer(inf_edges=False, max_depth=6, min_samples_leaf=x.shape[0] - 1, random_state=42).fit( - x, y - ) - assert myTreeBucketer.bin_count == 1 - assert all([x >= myTreeBucketer.tree.min_samples_leaf for x in myTreeBucketer.counts_]) - - -def test_tree_binning(): - """ - Test binning with a decisiontree. - """ - x = [1, 2, 2, 5, 3] - y = [0, 0, 1, 1, 1] - myBucketer = TreeBucketer(inf_edges=True, max_depth=2, min_impurity_decrease=0.001) - myBucketer.fit(x, y) - assert myBucketer.boundaries_ == [-np.inf, 1.5, 2.5, np.inf] - assert myBucketer.bin_count == 3 - assert myBucketer.counts_ == [1, 2, 2] - - myBucketer = TreeBucketer(max_depth=2, min_impurity_decrease=0.001) - myBucketer.fit(x, y) - assert myBucketer.boundaries_ == [1, 1.5, 2.5, 5] - assert myBucketer.bin_count == 3 - assert myBucketer.counts_ == [1, 2, 2] - - -def test_compute_counts_per_bin(): - """ - Test for checking if counts per bin are correctly computed. - """ - x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - boundaries = [0, 1, 5, 6, 10, 11] # down boundary < current value <= up boundary - np.testing.assert_array_almost_equal(Bucketer._compute_counts_per_bin(x, boundaries), np.array([1, 4, 1, 4, 0])) diff --git a/tests/docs/test_docstring.py b/tests/docs/test_docstring.py index c53e840d..70e98ff9 100644 --- a/tests/docs/test_docstring.py +++ b/tests/docs/test_docstring.py @@ -7,13 +7,9 @@ import matplotlib.pyplot as plt import pytest -import probatus.binning import probatus.feature_elimination import probatus.interpret -import probatus.metric_volatility -import probatus.missing_values import probatus.sample_similarity -import probatus.stat_tests import probatus.utils # Turn off interactive mode in plots @@ -21,22 +17,11 @@ matplotlib.use("Agg") CLASSES_TO_TEST = [ - probatus.binning.SimpleBucketer, - probatus.binning.AgglomerativeBucketer, - probatus.binning.QuantileBucketer, - probatus.binning.TreeBucketer, probatus.feature_elimination.ShapRFECV, probatus.interpret.DependencePlotter, - probatus.interpret.ShapModelInterpreter, - probatus.metric_volatility.TrainTestVolatility, - probatus.metric_volatility.BootstrappedVolatility, - probatus.metric_volatility.SplitSeedVolatility, probatus.sample_similarity.SHAPImportanceResemblance, probatus.sample_similarity.PermutationImportanceResemblance, - probatus.stat_tests.DistributionStatistics, - probatus.stat_tests.AutoDist, probatus.utils.Scorer, - probatus.missing_values.ImputationSelector, ] CLASSES_TO_TEST_LGBM = [ diff --git a/tests/interpret/test_inspector.py b/tests/interpret/test_inspector.py deleted file mode 100644 index 5ce76a6c..00000000 --- a/tests/interpret/test_inspector.py +++ /dev/null @@ -1,784 +0,0 @@ -from unittest.mock import patch - -import numpy as np -import pandas as pd -import pytest - -from probatus.interpret.inspector import BaseInspector, InspectorShap, return_confusion_metric -from probatus.utils import NotFittedError, UnsupportedModelError -from tests.mocks import MockClusterer, MockModel - -test_sensitivity = 0.0000000001 - - -@pytest.mark.skip(reason="Not currently implemented") -def test_after_implementation_completed(): - """ - Test. - """ - - @pytest.fixture(scope="function") - def global_clusters(): - return pd.Series([1, 2, 3, 4, 1, 2, 3, 4], name="cluster_id") - - @pytest.fixture(scope="function") - def global_clusters_eval_set(): - return [pd.Series([1, 2, 3], name="cluster_id"), pd.Series([1, 2, 3], name="cluster_id")] - - @pytest.fixture(scope="function") - def global_y(): - return pd.Series([0, 1, 1, 0, 0, 0, 1, 0]) - - @pytest.fixture(scope="function") - def global_X(): - return pd.DataFrame([[0], [1], [1], [0], [0], [0], [1], [0]]) - - @pytest.fixture(scope="function") - def global_confusion_metric(): - return pd.Series([0.1, 0.8, 0.3, 0.1, 0.1, 0.3, 0.3, 0.1]) - - @pytest.fixture(scope="function") - def global_summary_df(columns_summary_df): - return pd.DataFrame( - [ - [1, 0, 0.1, 0.1], - [2, 1, 0.2, 0.8], - [3, 1, 0.7, 0.3], - [4, 0, 0.1, 0.1], - [1, 0, 0.1, 0.1], - [2, 0, 0.3, 0.3], - [3, 1, 0.7, 0.3], - [4, 0, 0.1, 0.1], - ], - columns=columns_summary_df, - ) - - @pytest.fixture(scope="function") - def global_X_shap(): - return pd.DataFrame( - [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1], - [2, 0, 0, 0], - [0, 2, 0, 0], - [0, 0, 2, 0], - [0, 0, 0, 2], - ], - columns=["shap1", "shap2", "shap3", "shap4"], - ) - - @pytest.fixture(scope="function") - def columns_aggregate_summary_df(): - return [ - "cluster_id", - "total_label_1", - "total_entries", - "label_1_rate", - "average_confusion", - "average_pred_proba", - ] - - @pytest.fixture(scope="function") - def columns_summary_df(): - return ["cluster_id", "target", "pred_proba", "confusion"] - - @pytest.fixture(scope="function") - def global_aggregate_summary_df(columns_aggregate_summary_df): - return pd.DataFrame( - [[1, 0, 2, 0, 0.1, 0.1], [2, 1, 2, 0.5, 0.55, 0.25], [3, 2, 2, 1, 0.3, 0.7], [4, 0, 2, 0, 0.1, 0.1]], - columns=columns_aggregate_summary_df, - ) - - @pytest.fixture(scope="function") - def global_aggregate_summary_dfs_eval_set(columns_aggregate_summary_df): - return [ - pd.DataFrame( - [[1, 0, 1, 0, 0.1, 0.1], [2, 0, 1, 0, 0.2, 0.2], [3, 0, 1, 0, 0.3, 0.3]], - columns=columns_aggregate_summary_df, - ), - pd.DataFrame( - [[1, 1, 1, 1, 0.4, 0.6], [2, 1, 1, 1, 0.5, 0.5], [3, 1, 1, 1, 0.6, 0.4]], - columns=columns_aggregate_summary_df, - ), - ] - - @pytest.fixture(scope="function") - def global_summary_dfs(): - return [ - pd.DataFrame([[1, 2, 3], [2, 3, 4], [3, 4, 5]], columns=["cluster_id", "column_a", "column_b"]), - pd.DataFrame([[1, 2, 1], [2, 3, 2], [3, 4, 3]], columns=["cluster_id", "column_a", "column_b"]), - ] - - @pytest.fixture(scope="function") - def global_X_shaps(): - return [ - pd.DataFrame([[0, 3, 0], [3, 0, 0], [0, 0, 3]], columns=["shap_1", "shap_2", "shap_3"]), - pd.DataFrame([[0, 2, 0], [2, 0, 0], [0, 0, 2]], columns=["shap_1", "shap_2", "shap_3"]), - ] - - @pytest.fixture(scope="function") - def global_ys(): - return [pd.Series([0, 0, 0]), pd.Series([1, 1, 1])] - - @pytest.fixture(scope="function") - def global_Xs(): - return [pd.DataFrame([[0], [1], [1]]), pd.DataFrame([[0], [1], [1]])] - - @pytest.fixture(scope="function") - def global_predicted_probas(): - return [pd.Series([0.1, 0.2, 0.3]), pd.Series([0.4, 0.5, 0.6])] - - @pytest.fixture(scope="function") - def global_predicted_proba(): - return pd.Series([0.1, 0.2, 0.7, 0.1, 0.1, 0.3, 0.7, 0.1], name="pred_proba") - - @pytest.fixture(scope="function") - def global_small_df(): - return pd.DataFrame([[1, 2, 3, 4], [1, 2, 3, 4]]) - - @pytest.fixture(scope="function") - def global_small_df_flat(): - return pd.Series([1, 2, 3, 4]) - - @pytest.fixture(scope="function") - def global_mock_aggregate_summary_dfs(): - return [ - pd.DataFrame([[1, 3], [2, 3]], columns=["cluster_id", "column_a"]), - pd.DataFrame([[1, 2], [2, 3]], columns=["cluster_id", "column_a"]), - ] - - @pytest.fixture(scope="function") - def global_mock_summary_df(): - return pd.DataFrame([[1, 2], [2, 3]], columns=["cluster_id", "column_a"]) - - def test_return_confusion_metric__array(): - y_true = np.array([0, 0, 0, 1, 1, 1], dtype=float) - y_score = np.array([0.1, 0.2, 0.3, 0.7, 0.8, 0.9], dtype=float) - - expected_output_not_normalized = np.array([0.1, 0.2, 0.3, 0.3, 0.2, 0.1], dtype=float) - expected_output_normalized = np.array( - [0.11111111, 0.22222222, 0.33333333, 0.22222222, 0.11111111, 0.0], dtype=float - ) - assert ( - expected_output_normalized - return_confusion_metric(y_true, y_score, normalize=True < test_sensitivity) - ).all() - assert ( - expected_output_not_normalized - return_confusion_metric(y_true, y_score, normalize=False) - < test_sensitivity - ).all() - - def test_return_confusion_metric__series(): - # The method also needs to work with series, since it is called with series by create summary df - y_true = pd.Series([0, 0, 0, 1, 1, 1]) - y_score = pd.Series([0.1, 0.2, 0.3, 0.7, 0.8, 0.9]) - - expected_output_not_normalized = pd.Series([0.1, 0.2, 0.3, 0.3, 0.2, 0.1], dtype=float) - expected_output_normalized = pd.Series( - [0.11111111, 0.22222222, 0.33333333, 0.22222222, 0.11111111, 0.0], dtype=float - ) - assert ( - expected_output_normalized - return_confusion_metric(y_true, y_score, normalize=True < test_sensitivity) - ).all() - assert ( - expected_output_not_normalized - return_confusion_metric(y_true, y_score, normalize=False) - < test_sensitivity - ).all() - - @patch.object(MockClusterer, "fit") - def test_fit_clusters__base_inspector(mock_clusterer, global_small_df): - # Base Inspector case algotype is kmeans - inspector = BaseInspector(algotype="kmeans") - inspector.clusterer = mock_clusterer - - X = global_small_df - - inspector.fit_clusters(X) - - # Check if has been called with correct argument - mock_clusterer.fit.assert_called_once() - pd.testing.assert_frame_equal(mock_clusterer.fit.call_args[0][0], X) - # Check if it has not been modified - pd.testing.assert_frame_equal(X, global_small_df) - # Check if fitted flag has been changed correctly - assert inspector.fitted is True - - @patch.object(MockClusterer, "fit") - def test_fit_clusters__inspector_shap(mock_clusterer, global_small_df): - inspector = InspectorShap(model=MockModel(), algotype="kmeans", cluster_probability=False) - inspector.clusterer = mock_clusterer - - X = global_small_df - - inspector.fit_clusters(X) - - # Check if has been called with correct argument - mock_clusterer.fit.assert_called_once() - pd.testing.assert_frame_equal(mock_clusterer.fit.call_args[0][0], X) - # Check if it has not been modified - pd.testing.assert_frame_equal(X, global_small_df) - # Check if fitted flag has been changed correctly - assert inspector.fitted is True - - @patch.object(MockClusterer, "fit") - def test_fit_clusters__inspector_shap_proba(mock_clusterer, global_small_df): - inspector = InspectorShap(model=MockModel(), algotype="kmeans", cluster_probability=True) - inspector.clusterer = mock_clusterer - inspector.predicted_proba = True - - X = global_small_df - - # Check if not fitted exception is raised - inspector.fit_clusters(X) - - # Check if column with probabilities has been added to the fitted X - assert "probs" in mock_clusterer.fit.call_args[0][0].columns - - # Check if has been called - mock_clusterer.fit.assert_called_once() - - # Check if X has not been modified - pd.testing.assert_frame_equal(X, global_small_df) - assert inspector.fitted is True - - @patch.object(MockClusterer, "predict") - def test_predict_clusters__base_inspector(mock_clusterer, global_small_df): - mock_clusterer.predict.return_value = [1, 0] - - inspector = BaseInspector(algotype="kmeans") - inspector.clusterer = mock_clusterer - inspector.fitted = True - - X = global_small_df - - # Check if the prediction is correct according to the Mock clusterer - assert inspector.predict_clusters(X) == [1, 0] - - # Check if the clusterer was called with correct input - mock_clusterer.predict.assert_called_once() - pd.testing.assert_frame_equal(mock_clusterer.predict.call_args[0][0], X) - - # Check if the X has not been modified - pd.testing.assert_frame_equal(X, global_small_df) - - @patch.object(MockClusterer, "predict") - def test_predict_clusters__inspector_shap(mock_clusterer, global_small_df): - mock_clusterer.predict.return_value = [1, 0] - - inspector = InspectorShap(model=MockModel(), algotype="kmeans", cluster_probability=False) - inspector.clusterer = mock_clusterer - inspector.fitted = True - - X = global_small_df - - # Check if the output is correct, as should be according to MockClusterer - assert inspector.predict_clusters(X) == [1, 0] - # Check if the df has not been modified by the prediction - pd.testing.assert_frame_equal(X, global_small_df) - - @patch.object(MockClusterer, "predict") - def test_predict_clusters__not_fitted(mock_clusterer, global_small_df): - mock_clusterer.predict.return_value = [1, 0] - - # InspectorShap not fitted - inspector = InspectorShap(model=MockModel(), algotype="kmeans", cluster_probability=True) - inspector.clusterer = mock_clusterer - inspector.predicted_proba = True - - X = global_small_df - - # Check if not fitted exception is raised - with pytest.raises(NotFittedError): - inspector.predict_clusters(X) - # Check if X3 has not been modified - pd.testing.assert_frame_equal(X, global_small_df) - - def test_assert_is_dataframe(global_small_df): - X_df = global_small_df - X_list = X_df.values.tolist() - X_array = np.asarray(X_list) - X_array_flat = np.asarray(X_list[0]) - - pd.testing.assert_frame_equal(X_df, BaseInspector.assert_is_dataframe(X_df)) - pd.testing.assert_frame_equal(X_df, BaseInspector.assert_is_dataframe(X_array)) - with pytest.raises(NotImplementedError): - BaseInspector.assert_is_dataframe(X_list) - with pytest.raises(NotImplementedError): - BaseInspector.assert_is_dataframe(X_array_flat) - - def test_assert_is_series(global_small_df, global_small_df_flat): - X_df = global_small_df - X_df_flat = global_small_df_flat - X_list = X_df.values.tolist() - X_list_flat = X_df_flat.values.tolist() - - X_series = pd.Series(X_list_flat) - X_array = np.asarray(X_list) - X_array_flat = np.asarray(X_list_flat) - index = [0, 1, 2, 3] - - pd.testing.assert_series_equal(X_series, BaseInspector.assert_is_series(X_series)) - pd.testing.assert_series_equal(X_series, BaseInspector.assert_is_series(X_df_flat)) - pd.testing.assert_series_equal(X_series, BaseInspector.assert_is_series(X_array_flat, index=index)) - - with pytest.raises(TypeError): - BaseInspector.assert_is_series(X_list) - with pytest.raises(TypeError): - BaseInspector.assert_is_series(X_list_flat) - with pytest.raises(TypeError): - BaseInspector.assert_is_series(X_df) - with pytest.raises(TypeError): - BaseInspector.assert_is_series(X_array) - with pytest.raises(TypeError): - BaseInspector.assert_is_series(X_array, index=[0, 1]) - with pytest.raises(TypeError): - BaseInspector.assert_is_series(X_array_flat) - - def test_get_cluster_mask(global_summary_df): - df = global_summary_df - cluster_id_1 = 1 - cluster_id_2 = [1, 4] - - expected_indexes_1 = [0, 4] - expected_indexes_2 = [0, 3, 4, 7] - - pd.testing.assert_frame_equal(df.iloc[expected_indexes_1], df[InspectorShap.get_cluster_mask(df, cluster_id_1)]) - pd.testing.assert_frame_equal(df.iloc[expected_indexes_2], df[InspectorShap.get_cluster_mask(df, cluster_id_2)]) - - @patch("probatus.interpret.inspector.return_confusion_metric") - def test_create_summary_df( - mocked_method, global_clusters, global_y, global_predicted_proba, global_confusion_metric, global_summary_df - ): - cluster_series = global_clusters - y_series = global_y - probas = global_predicted_proba - - mocked_method.return_value = global_confusion_metric - expected_output = global_summary_df - - output = InspectorShap.create_summary_df(cluster_series, y_series, probas, normalize=False) - - # Check if method is called with correct input - mocked_method.assert_called_once() - pd.testing.assert_series_equal(mocked_method.call_args[0][0], y_series) - pd.testing.assert_series_equal(mocked_method.call_args[0][1], probas) - assert mocked_method.call_args_list[0][1]["normalize"] is False - - # Check if the output is correct - pd.testing.assert_frame_equal(output, expected_output) - - def test_aggregate_summary_df(global_summary_df, global_aggregate_summary_df): - df = global_summary_df - expected_output = global_aggregate_summary_df - pd.set_option("display.max_columns", None) - - pd.testing.assert_frame_equal(InspectorShap.aggregate_summary_df(df), expected_output) - - def test_compute__report_done(): - inspector = InspectorShap(model=MockModel(), algotype="kmeans", cluster_probability=False) - report_value = pd.DataFrame([[1, 2], [2, 3]], columns=["cluster_id", "column_a"]) - inspector.cluster_report = report_value - - pd.testing.assert_frame_equal(inspector.compute(), report_value) - - def test_compute__single_df(global_mock_summary_df): - inspector = InspectorShap(model=MockModel(), algotype="kmeans", cluster_probability=False) - inspector.hasmultiple_dfs = False - - report_value = global_mock_summary_df - - def mock_compute_report(self): - self.agg_summary_df = report_value - - with patch.object(InspectorShap, "_compute_report", mock_compute_report): - output = inspector.compute() - - # Check output and side effects - pd.testing.assert_frame_equal(output, report_value) - pd.testing.assert_frame_equal(inspector.cluster_report, report_value) - pd.testing.assert_frame_equal(inspector.agg_summary_df, report_value) - - def test_compute__multiple_df(global_mock_summary_df, global_mock_aggregate_summary_dfs): - inspector = InspectorShap(model=MockModel(), algotype="kmeans", cluster_probability=False) - inspector.hasmultiple_dfs = True - - report_value = global_mock_summary_df - inspector.agg_summary_dfs = global_mock_aggregate_summary_dfs - - expected_result = pd.DataFrame( - [[1, 2, 3, 2], [2, 3, 3, 3]], columns=["cluster_id", "column_a", "column_a_sample_1", "column_a_sample_2"] - ) - - def mock_compute_report(self): - self.agg_summary_df = report_value - - with patch.object(InspectorShap, "_compute_report", mock_compute_report): - output = inspector.compute() - - # Check output and side effects - pd.testing.assert_frame_equal(output, expected_result) - pd.testing.assert_frame_equal(inspector.cluster_report, expected_result) - pd.testing.assert_frame_equal(inspector.agg_summary_df, report_value) - - def test_compute__multiple_df_set_names(global_mock_summary_df, global_mock_aggregate_summary_dfs): - inspector = InspectorShap(model=MockModel(), algotype="kmeans", cluster_probability=False) - inspector.hasmultiple_dfs = True - inspector.set_names = ["suf1", "suf2"] - - report_value = global_mock_summary_df - inspector.agg_summary_dfs = global_mock_aggregate_summary_dfs - - expected_result = pd.DataFrame( - [[1, 2, 3, 2], [2, 3, 3, 3]], columns=["cluster_id", "column_a", "column_a_suf1", "column_a_suf2"] - ) - - def mock_compute_report(self): - self.agg_summary_df = report_value - - with patch.object(InspectorShap, "_compute_report", mock_compute_report): - output = inspector.compute() - - # Check output and side effects - pd.testing.assert_frame_equal(output, expected_result) - pd.testing.assert_frame_equal(inspector.cluster_report, expected_result) - pd.testing.assert_frame_equal(inspector.agg_summary_df, report_value) - - def test_slice_cluster_no_inputs_not_complementary( - global_summary_df, global_X_shap, global_y, global_predicted_proba - ): - inspector = InspectorShap(model=MockModel(), algotype="kmeans") - summary = global_summary_df - inspector.summary_df = summary - inspector.cluster_report = summary - inspector.X_shap = X_shap = global_X_shap - inspector.y = y = global_y - inspector.predicted_proba = predicted_proba = global_predicted_proba - - target_cluster_id = 1 - correct_mask = returned_mask = [True, False, False, False, True, False, False, False] - inspector.get_cluster_mask.return_value = correct_mask - - with patch.object(InspectorShap, "compute") as mocked_compute: - with patch.object(InspectorShap, "get_cluster_mask") as mock_get_cluster_mask: - mock_get_cluster_mask.return_value = returned_mask - shap_out, y_out, pred_out = inspector.slice_cluster(target_cluster_id, complementary=False) - - # Ensure mocked_compute not called - mocked_compute.accert_not_called() - # Ensure mock_get_cluster_mask called with correct arguments - mock_get_cluster_mask.assert_called_once() - pd.testing.assert_frame_equal(mock_get_cluster_mask.call_args[0][0], summary) - assert mock_get_cluster_mask.call_args[0][1] == target_cluster_id - - # Check outputs - pd.testing.assert_frame_equal(shap_out, X_shap[correct_mask]) - pd.testing.assert_series_equal(y_out, y[correct_mask]) - pd.testing.assert_series_equal(pred_out, predicted_proba[correct_mask]) - - def test_slice_cluster_inputs_complementary(global_summary_df, global_X_shap, global_y, global_predicted_proba): - inspector = InspectorShap(model=MockModel(), algotype="kmeans") - summary = global_summary_df - X_shap = global_X_shap - y = global_y - predicted_proba = global_predicted_proba - - target_cluster_id = 1 - correct_mask = np.array([False, True, True, True, False, True, True, True]) - returned_mask = np.logical_not(correct_mask) - inspector.get_cluster_mask.return_value = correct_mask - - assert inspector.cluster_report is None - - def mock_compute(self): - self.cluster_report = summary - - with patch.object(InspectorShap, "compute", mock_compute): - with patch.object(InspectorShap, "get_cluster_mask") as mock_get_cluster_mask: - mock_get_cluster_mask.return_value = returned_mask - shap_out, y_out, pred_out = inspector.slice_cluster( - target_cluster_id, - summary_df=summary, - X_shap=X_shap, - y=y, - predicted_proba=predicted_proba, - complementary=True, - ) - # Ensure mocked_get_cluster_mask called with correct arguments - mock_get_cluster_mask.assert_called_once() - pd.testing.assert_frame_equal(mock_get_cluster_mask.call_args[0][0], summary) - assert mock_get_cluster_mask.call_args[0][1] == target_cluster_id - - # Check outputs and side effects - pd.testing.assert_frame_equal(shap_out, X_shap[correct_mask]) - pd.testing.assert_series_equal(y_out, y[correct_mask]) - pd.testing.assert_series_equal(pred_out, predicted_proba[correct_mask]) - pd.testing.assert_frame_equal(inspector.cluster_report, summary) - - def test_init_inspector(): - mock_model = MockModel() - inspector = InspectorShap( - model=mock_model, - algotype="kmeans", - confusion_metric="proba", - normalize_probability=True, - cluster_probability=True, - ) - assert inspector.model is mock_model - assert inspector.isinspected is False - assert inspector.hasmultiple_dfs is False - assert inspector.normalize_proba is True - assert inspector.cluster_probabilities is True - assert inspector.agg_summary_df is None - assert inspector.set_names is None - assert inspector.confusion_metric == "proba" - assert inspector.cluster_report is None - assert inspector.y is None - assert inspector.predicted_proba is None - assert inspector.X_shap is None - assert inspector.clusters is None - assert inspector.algotype == "kmeans" - assert inspector.fitted is False - assert inspector.X_shaps == list() - assert inspector.clusters_list == list() - assert inspector.ys == list() - assert inspector.predicted_probas == list() - - def test_init_inspector_error(): - with pytest.raises(NotImplementedError): - InspectorShap(model=MockModel(), algotype="kmeans", confusion_metric="error") - - def test_init_inspector_error2(): - with pytest.raises(UnsupportedModelError): - InspectorShap(model=MockModel(), algotype="error", confusion_metric="proba") - - def test_slice_cluster_eval_sets__single_df(): - inspector = InspectorShap(model=MockModel(), algotype="kmeans") - inspector.hasmultiple_dfs = False - cluster_id = 1 - with pytest.raises(NotFittedError): - inspector.slice_cluster_eval_set(cluster_id) - - def test_slice_cluster_eval_sets__multiple_df( - global_X_shaps, global_ys, global_predicted_probas, global_summary_dfs - ): - inspector = InspectorShap(model=MockModel(), algotype="kmeans") - inspector.hasmultiple_dfs = True - - inspector.X_shaps = X_shaps = global_X_shaps - inspector.ys = ys = global_ys - inspector.predicted_probas = predicted_probas = global_predicted_probas - inspector.summary_dfs = summary_dfs = global_summary_dfs - - target_row = [0] - target_cluster_id = 1 - target_complementary = False - - target_output = [ - [pd.DataFrame([[0, 3, 0]], columns=["shap_1", "shap_2", "shap_3"]), pd.Series([0]), pd.Series([0.1])], - [pd.DataFrame([[0, 2, 0]], columns=["shap_1", "shap_2", "shap_3"]), pd.Series([1]), pd.Series([0.4])], - ] - - with patch.object(InspectorShap, "slice_cluster") as mock_slice_cluster: - # Setting multiple outputs - mock_slice_cluster.side_effect = [ - (X_shaps[0].iloc[target_row], ys[0].iloc[target_row], predicted_probas[0].iloc[target_row]), - (X_shaps[1].iloc[target_row], ys[1].iloc[target_row], predicted_probas[1].iloc[target_row]), - ] - - output = inspector.slice_cluster_eval_set(target_cluster_id, complementary=target_complementary) - - # Check if inputs are correct at each call - for call_index, call in enumerate(mock_slice_cluster.call_args_list): - # On the position 1 of call there are kwargs - assert call[1]["cluster_id"] == target_cluster_id - assert call[1]["complementary"] == target_complementary - pd.testing.assert_frame_equal(call[1]["summary_df"], summary_dfs[call_index]) - pd.testing.assert_frame_equal(call[1]["X_shap"], X_shaps[call_index]) - pd.testing.assert_series_equal(call[1]["predicted_proba"], predicted_probas[call_index]) - pd.testing.assert_series_equal(call[1]["y"], ys[call_index]) - - # Check lengths of lists - assert len(output) is len(target_output) - - # Go over the output and check each element - for index, current_output in enumerate(output): - pd.testing.assert_frame_equal(target_output[index][0], current_output[0]) - pd.testing.assert_series_equal(target_output[index][1], current_output[1]) - pd.testing.assert_series_equal(target_output[index][2], current_output[2]) - - def test_compute_report_single_df( - global_clusters, global_y, global_predicted_proba, global_summary_df, global_aggregate_summary_df - ): - inspector = InspectorShap(model=MockModel(), algotype="kmeans") - inspector.hasmultiple_dfs = False - inspector.normalize_proba = target_normalize = False - - inspector.clusters = input_clust = global_clusters - inspector.y = input_y = global_y - inspector.predicted_proba = input_predicted_proba = global_predicted_proba - target_summary_df = global_summary_df - aggregated_summary = global_aggregate_summary_df - - with patch.object(InspectorShap, "create_summary_df") as mock_create_summary_df: - with patch.object(InspectorShap, "aggregate_summary_df") as mock_aggregate_summary_df: - mock_create_summary_df.return_value = target_summary_df - mock_aggregate_summary_df.return_value = aggregated_summary - - inspector._compute_report() - - # check if the methods were called with correct arguments - pd.testing.assert_frame_equal(mock_aggregate_summary_df.call_args[0][0], target_summary_df) - pd.testing.assert_series_equal(mock_create_summary_df.call_args[0][0], input_clust) - pd.testing.assert_series_equal(mock_create_summary_df.call_args[0][1], input_y) - pd.testing.assert_series_equal(mock_create_summary_df.call_args[0][2], input_predicted_proba) - assert mock_create_summary_df.call_args[1]["normalize"] == target_normalize - - # Check if the function correctly stored variables - pd.testing.assert_frame_equal(inspector.agg_summary_df, aggregated_summary) - pd.testing.assert_frame_equal(inspector.summary_df, target_summary_df) - - def test_compute_report_multiple_df( - global_clusters, - global_y, - global_predicted_proba, - global_summary_df, - global_aggregate_summary_df, - global_summary_dfs, - global_ys, - global_predicted_probas, - global_aggregate_summary_dfs_eval_set, - ): - inspector = InspectorShap(model=MockModel(), algotype="kmeans") - inspector.hasmultiple_dfs = True - inspector.normalize_proba = False - - inspector.clusters = global_clusters - inspector.y = global_y - inspector.predicted_proba = global_predicted_proba - inspector.ys = global_ys - inspector.predicted_probas = global_predicted_probas - target_summary_df = global_summary_df - target_summary_dfs = global_summary_dfs - aggregated_summary_df = global_aggregate_summary_df - aggregated_summary_dfs = global_aggregate_summary_dfs_eval_set - - with patch.object(InspectorShap, "create_summary_df") as mock_create_summary_df: - with patch.object(InspectorShap, "aggregate_summary_df") as mock_aggregate_summary_df: - # Set returns for each call of methods - mock_create_summary_df.side_effect = [target_summary_df, target_summary_dfs[0], target_summary_dfs[1]] - mock_aggregate_summary_df.side_effect = [ - aggregated_summary_df, - aggregated_summary_dfs[0], - aggregated_summary_dfs[1], - ] - inspector._compute_report() - - assert inspector.agg_summary_df.equals(aggregated_summary_df) - assert inspector.summary_df.equals(target_summary_df) - for index, item in inspector.agg_summary_dfs: - assert item.equals(aggregated_summary_dfs[index]) - for index, item in inspector.summary_dfs: - assert item.equals(target_summary_dfs[index]) - - def test_perform_fit_calc(global_X, global_y, global_predicted_proba, global_X_shap, global_clusters): - inspector = InspectorShap(model=MockModel(), algotype="kmeans") - inspector.model = MockModel() - input_X = global_X - input_y = global_y - input_predicted_proba = global_predicted_proba - values_probabilities = input_predicted_proba.tolist() - - def mock_fit_clusters(self, X_shap): - inspector.fitted = True - - with patch.object(InspectorShap, "assert_is_dataframe") as mock_assert_is_dataframe: - with patch.object(InspectorShap, "assert_is_series") as mock_assert_is_series: - with patch.object(InspectorShap, "compute_probabilities") as mock_compute_probabilities: - with patch("probatus.interpret._shap_helpers.shap_to_df") as mock_shap_to_df: - with patch.object(InspectorShap, "fit_clusters", mock_fit_clusters): - with patch.object(InspectorShap, "predict_clusters") as mock_predict_clusters: - mock_assert_is_dataframe.return_value = input_X - mock_assert_is_series.return_value = input_y - mock_compute_probabilities.return_value = values_probabilities - mock_shap_to_df.return_value = global_X_shap - mock_predict_clusters.return_value = global_clusters.tolist() - - out_y, out_predicted_proba, out_X_shap, out_clusters = inspector.perform_fit_calc( - input_X, input_y, fit_clusters=True - ) - - pd.testing.assert_series_equal(out_y, input_y) - pd.testing.assert_series_equal(out_predicted_proba, input_predicted_proba) - pd.testing.assert_frame_equal(out_X_shap, global_X_shap) - pd.testing.assert_series_equal(out_clusters, global_clusters) - assert inspector.fitted is True - - def test_fit__multiple_df( - global_X, - global_y, - global_predicted_proba, - global_X_shap, - global_clusters, - global_Xs, - global_ys, - global_predicted_probas, - global_clusters_eval_set, - global_X_shaps, - ): - inspector = InspectorShap(model=MockModel(), algotype="kmeans") - input_eval_set = [(global_Xs[0], global_ys[0]), (global_Xs[1], global_ys[1])] - input_sample_names = ["set1", "set2"] - input_X = global_X - input_y = global_y - - with patch.object(InspectorShap, "perform_fit_calc") as mock_perform_fit_calc: - with patch.object(InspectorShap, "init_eval_set_report_variables") as mock_init_variables: - mock_perform_fit_calc.side_effect = [ - (global_y, global_predicted_proba, global_X_shap, global_clusters), - (global_ys[0], global_predicted_probas[0], global_X_shaps[0], global_clusters_eval_set[0]), - (global_ys[1], global_predicted_probas[1], global_X_shaps[1], global_clusters_eval_set[1]), - ] - - inspector.fit(X=input_X, y=input_y, eval_set=input_eval_set, sample_names=input_sample_names) - mock_init_variables.assert_called_once() - - assert inspector.hasmultiple_dfs is True - assert inspector.set_names is input_sample_names - assert inspector.y.equals(global_y) - assert inspector.predicted_proba.equals(global_predicted_proba) - assert inspector.X_shap.equals(global_X_shap) - assert inspector.clusters.equals(global_clusters) - assert all([a.equals(b) for a, b in zip(inspector.clusters_list, global_clusters_eval_set)]) - assert all([a.equals(b) for a, b in zip(inspector.X_shaps, global_X_shaps)]) - assert all([a.equals(b) for a, b in zip(inspector.predicted_probas, global_predicted_probas)]) - assert all([a.equals(b) for a, b in zip(inspector.ys, global_ys)]) - assert input_sample_names is inspector.set_names - - def test_compute_probabilities(global_X): - inspector = InspectorShap(model=MockModel(), algotype="kmeans") - input_X = global_X - model_probas = np.array( - [[0.2, 0.8], [0.7, 0.3], [0.7, 0.3], [0.3, 0.7], [0.2, 0.8], [0.3, 0.7], [0.7, 0.3], [0.5, 0.5]] - ) - expected_output = np.array([0.8, 0.3, 0.3, 0.7, 0.8, 0.7, 0.3, 0.5]) - - with patch.object(MockModel, "predict_proba") as mock_predict_proba: - mock_predict_proba.return_value = model_probas - np.testing.assert_array_equal(expected_output, inspector.compute_probabilities(input_X)) - - def test_fit_compute(global_X, global_aggregate_summary_df): - inspector = InspectorShap(model=MockModel(), algotype="kmeans") - input_X = global_X - expected_output = global_aggregate_summary_df - - with patch.object(InspectorShap, "fit") as mock_fit: - with patch.object(InspectorShap, "compute") as mock_compute: - mock_compute.return_value = global_aggregate_summary_df - - output = inspector.fit_compute(input_X) - - # Check if fit called with input X - pd.testing.assert_frame_equal(mock_fit.call_args[0][0], input_X) - # Check if the returned value correct - pd.testing.assert_frame_equal(expected_output, output) diff --git a/tests/interpret/test_shap_dependence.py b/tests/interpret/test_shap_dependence.py index 9a37468a..5e0e6fa3 100644 --- a/tests/interpret/test_shap_dependence.py +++ b/tests/interpret/test_shap_dependence.py @@ -121,8 +121,7 @@ def test_fit_complex(complex_data_split, complex_fitted_lightgbm): assert plotter.fitted is True # Check if plotting does not cause errors - for binning in ["simple", "agglomerative", "quantile"]: - _ = plotter.plot(feature="f2_missing", type_binning=binning, show=False) + _ = plotter.plot(feature="f2_missing", show=False) def test_get_X_y_shap_with_q_cut_normal(X_y, clf): @@ -182,8 +181,7 @@ def test_plot_normal(X_y, clf): Test. """ plotter = DependencePlotter(clf).fit(X_y[0], X_y[1]) - for binning in ["simple", "agglomerative", "quantile"]: - _ = plotter.plot(feature=0, type_binning=binning) + _ = plotter.plot(feature=0) def test_plot_class_names(X_y, clf): @@ -202,8 +200,8 @@ def test_plot_input(X_y, clf): plotter = DependencePlotter(clf).fit(X_y[0], X_y[1]) with pytest.raises(ValueError): plotter.plot(feature="not a feature") - with pytest.raises(ValueError): - plotter.plot(feature=0, type_binning=5) + with pytest.raises(TypeError): + plotter.plot(feature=0, bins=5.0) with pytest.raises(ValueError): plotter.plot(feature=0, min_q=1, max_q=0) diff --git a/tests/metric_volatility/__init__.py b/tests/metric_volatility/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/metric_volatility/test_metric_volatility.py b/tests/metric_volatility/test_metric_volatility.py deleted file mode 100644 index 2b832be6..00000000 --- a/tests/metric_volatility/test_metric_volatility.py +++ /dev/null @@ -1,422 +0,0 @@ -import os -from unittest.mock import patch - -import matplotlib -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import pytest -from sklearn.tree import DecisionTreeClassifier - -from probatus.metric_volatility import ( - BaseVolatilityEstimator, - BootstrappedVolatility, - SplitSeedVolatility, - TrainTestVolatility, - check_sampling_input, - get_metric, - sample_data, -) -from probatus.stat_tests.distribution_statistics import DistributionStatistics -from probatus.utils import NotFittedError, Scorer - -# Turn off interactive mode in plots -plt.ioff() -matplotlib.use("Agg") - - -@pytest.fixture(scope="function") -def X_array(): - """ - Fixture. - """ - return np.array([[2, 1], [3, 2], [4, 3], [1, 2], [1, 1]]) - - -@pytest.fixture(scope="function") -def y_list(): - """ - Fixture. - """ - return [1, 0, 0, 1, 1] - - -@pytest.fixture(scope="function") -def y_array(y_list): - """ - Fixture. - """ - return np.array(y_list) - - -@pytest.fixture(scope="function") -def X_df(X_array): - """ - Fixture. - """ - return pd.DataFrame(X_array, columns=["c1", "c2"]) - - -@pytest.fixture(scope="function") -def y_series(y_list): - """ - Fixture. - """ - return pd.Series(y_list) - - -@pytest.fixture(scope="function") -def iteration_results(): - """ - Fixture. - """ - iterations_cols = ["metric_name", "train_score", "test_score", "delta_score"] - return pd.DataFrame( - [ - ["roc_auc", 0.8, 0.7, 0.1], - ["roc_auc", 0.7, 0.6, 0.1], - ["roc_auc", 0.9, 0.8, 0.1], - ["accuracy", 1, 0.9, 0.1], - ["accuracy", 0.8, 0.7, 0.1], - ["accuracy", 0.9, 0.8, 0.1], - ], - columns=iterations_cols, - ) - - -@pytest.fixture(scope="function") -def report(): - """ - Fixture. - """ - report_cols = ["train_mean", "train_std", "test_mean", "test_std", "delta_mean", "delta_std"] - report_index = ["roc_auc", "accuracy"] - return pd.DataFrame( - [[0.8, 0.08164, 0.7, 0.08164, 0.1, 0], [0.9, 0.08164, 0.8, 0.08164, 0.1, 0]], - columns=report_cols, - index=report_index, - ).astype(float) - - -@pytest.fixture(scope="function") -def iterations_train(): - """ - Fixture. - """ - return pd.Series([0.8, 0.7, 0.9], name="train_score") - - -@pytest.fixture(scope="function") -def iterations_test(): - """ - Fixture. - """ - return pd.Series([0.7, 0.6, 0.8], name="test_score") - - -@pytest.fixture(scope="function") -def iterations_delta(): - """ - Fixture. - """ - return pd.Series([0.1, 0.1, 0.1], name="delta_score") - - -def test_inits(mock_model): - """ - Test. - """ - vol1 = SplitSeedVolatility( - mock_model, - scoring=["accuracy", "roc_auc"], - test_prc=0.3, - n_jobs=2, - stats_tests_to_apply=["ES", "KS"], - random_state=1, - iterations=20, - ) - - assert id(vol1.clf) == id(mock_model) - assert vol1.test_prc == 0.3 - assert vol1.n_jobs == 2 - assert vol1.stats_tests_to_apply == ["ES", "KS"] - assert vol1.random_state == 1 - assert vol1.iterations == 20 - assert len(vol1.stats_tests_objects) == 2 - assert len(vol1.scorers) == 2 - assert vol1.sample_train_test_split_seed is True - - vol2 = BootstrappedVolatility(mock_model, scoring="roc_auc", stats_tests_to_apply="KS", test_sampling_fraction=0.8) - - assert id(vol2.clf) == id(mock_model) - assert vol2.stats_tests_to_apply == ["KS"] - assert len(vol2.stats_tests_objects) == 1 - assert len(vol2.scorers) == 1 - assert vol2.sample_train_test_split_seed is False - assert vol2.test_sampling_fraction == 0.8 - assert vol2.fitted is False - assert vol2.iterations_results is None - assert vol2.report is None - - -def test_base_fit(mock_model, X_df, y_series): - """ - Test. - """ - vol = BaseVolatilityEstimator(mock_model, random_state=1) - - with patch("numpy.random.seed") as mock_seed: - vol.fit(X_df, y_series) - mock_seed.assert_called_with(1) - - assert vol.iterations_results is None - assert vol.report is None - assert vol.fitted is True - - -def test_compute(report, mock_model): - """ - Test. - """ - vol = BaseVolatilityEstimator(mock_model) - - with pytest.raises(NotFittedError): - vol.compute() - - vol.fit() - with pytest.raises(ValueError): - vol.compute() - - vol.report = report - - pd.testing.assert_frame_equal(vol.compute(), report) - pd.testing.assert_frame_equal(vol.compute(metrics=["roc_auc"]), report.loc[["roc_auc"]]) - pd.testing.assert_frame_equal(vol.compute(metrics="roc_auc"), report.loc[["roc_auc"]]) - - -def test_plot(report, mock_model, iterations_train, iterations_test, iterations_delta): - """ - Test. - """ - with patch.object(BaseVolatilityEstimator, "compute", return_value=report.loc[["roc_auc"]]) as mock_compute: - with patch.object( - BaseVolatilityEstimator, - "_get_samples_to_plot", - return_value=(iterations_train, iterations_test, iterations_delta), - ) as mock_get_samples: - vol = BaseVolatilityEstimator(mock_model) - vol.fitted = True - - vol.plot(metrics="roc_auc") - mock_compute.assert_called_with(metrics="roc_auc") - mock_get_samples.assert_called_with(metric_name="roc_auc") - - -def test_get_samples_to_plot(mock_model, iteration_results, iterations_train, iterations_test, iterations_delta): - """ - Test. - """ - vol = BaseVolatilityEstimator(mock_model) - vol.fitted = True - vol.iterations_results = iteration_results - - train, test, delta = vol._get_samples_to_plot(metric_name="roc_auc") - pd.testing.assert_series_equal(train, iterations_train) - pd.testing.assert_series_equal(test, iterations_test) - pd.testing.assert_series_equal(delta, iterations_delta) - - -def test_create_report(mock_model, iteration_results, report): - """ - Test. - """ - vol = BaseVolatilityEstimator(mock_model) - vol.fitted = True - vol.iterations_results = iteration_results - - vol._create_report() - pd.testing.assert_frame_equal(vol.report, report, atol=1e-3) - - -def test_compute_mean_std_from_runs(mock_model, iteration_results): - """ - Test. - """ - vol = BaseVolatilityEstimator(mock_model) - results = vol._compute_mean_std_from_runs(iteration_results[iteration_results["metric_name"] == "roc_auc"]) - expected_results = [0.8, 0.08164, 0.7, 0.08164, 0.1, 0] - for idx, item in enumerate(results): - assert pytest.approx(item, 0.01) == expected_results[idx] - - -def test_compute_stats_tests_values(mock_model, iteration_results): - """ - Test. - """ - vol = BaseVolatilityEstimator(mock_model, stats_tests_to_apply=["KS"]) - - with patch.object(DistributionStatistics, "compute", return_value=(0.1, 0.05)): - stats = vol._compute_stats_tests_values(iteration_results) - - assert stats[0] == 0.1 - assert stats[1] == 0.05 - - -def test_fit_compute(mock_model, report, X_df, y_series): - """ - Test. - """ - vol = BaseVolatilityEstimator(mock_model) - - with patch.object(BaseVolatilityEstimator, "fit") as mock_fit: - with patch.object(BaseVolatilityEstimator, "compute", return_value=report) as mock_compute: - result = vol.fit_compute(X_df, y_series) - - mock_fit.assert_called_with(X_df, y_series) - mock_compute.assert_called_with() - - pd.testing.assert_frame_equal(result, report) - - -def test_fit_train_test_sample_seed(mock_model, X_df, y_series, iteration_results): - """ - Test. - """ - vol = TrainTestVolatility(mock_model, scoring="roc_auc", iterations=3, sample_train_test_split_seed=True) - - with patch.object(BaseVolatilityEstimator, "fit") as mock_base_fit: - with patch.object(TrainTestVolatility, "_create_report") as mock_create_report: - with patch( - "probatus.metric_volatility.volatility.get_metric", - side_effect=[iteration_results.iloc[[0]], iteration_results.iloc[[1]], iteration_results.iloc[[2]]], - ): - vol.fit(X_df, y_series) - - mock_base_fit.assert_called_once() - mock_create_report.assert_called_once() - - pd.testing.assert_frame_equal(vol.iterations_results, iteration_results.iloc[[0, 1, 2]]) - - -def test_get_metric(mock_model, X_df, y_series): - """ - Test. - """ - split_seed = 1 - test_prc = 0.6 - with patch( - "probatus.metric_volatility.metric.train_test_split", - return_value=(X_df.iloc[[0, 1, 2]], X_df.iloc[[3, 4]], y_series.iloc[[0, 1, 2]], y_series.iloc[[3, 4]]), - ) as mock_split: - with patch( - "probatus.metric_volatility.metric.sample_data", - side_effect=[(X_df.iloc[[0, 1, 1]], y_series.iloc[[0, 1, 1]]), (X_df.iloc[[3, 3]], y_series.iloc[[3, 3]])], - ) as mock_sample: - with patch.object(Scorer, "score", side_effect=[0.8, 0.7]): - output = get_metric( - X_df, - y_series, - mock_model, - test_size=test_prc, - split_seed=split_seed, - scorers=[Scorer("roc_auc")], - train_sampling_type="bootstrap", - test_sampling_type="bootstrap", - train_sampling_fraction=1, - test_sampling_fraction=1, - ) - mock_split.assert_called_once() - mock_sample.assert_called() - mock_model.fit.assert_called() - - expected_output = pd.DataFrame( - [["roc_auc", 0.8, 0.7, 0.1]], columns=["metric_name", "train_score", "test_score", "delta_score"] - ) - pd.testing.assert_frame_equal(expected_output, output) - - -def test_sample_data_no_sampling(X_df, y_series): - """ - Test. - """ - with patch("probatus.metric_volatility.utils.check_sampling_input") as mock_sampling_input: - X_out, y_out = sample_data(X_df, y_series, sampling_type=None, sampling_fraction=1) - mock_sampling_input.assert_called_once() - pd.testing.assert_frame_equal(X_out, X_df) - pd.testing.assert_series_equal(y_out, y_series) - - -def test_sample_data_bootstrap(X_df, y_series): - """ - Test. - """ - with patch("probatus.metric_volatility.utils.check_sampling_input") as mock_sampling_input: - X_out, y_out = sample_data(X_df, y_series, sampling_type="bootstrap", sampling_fraction=0.8) - mock_sampling_input.assert_called_once() - assert X_out.shape == (4, 2) - assert y_out.shape == (4,) - - -def test_sample_data_sample(X_df, y_series): - """ - Test. - """ - with patch("probatus.metric_volatility.utils.check_sampling_input") as mock_sampling_input: - X_out, y_out = sample_data(X_df, y_series, sampling_type="subsample", sampling_fraction=1) - mock_sampling_input.assert_called_once() - pd.testing.assert_frame_equal(X_out, X_df) - pd.testing.assert_series_equal(y_out, y_series) - - -def test_check_sampling_input(X_array, y_array): - """ - Test. - """ - with pytest.raises(ValueError): - check_sampling_input("bootstrap", 0, "dataset") - with pytest.raises(ValueError): - check_sampling_input("subsample", 0, "dataset") - with pytest.raises(ValueError): - check_sampling_input("subsample", 1, "dataset") - with pytest.raises(ValueError): - check_sampling_input("subsample", 10, "dataset") - with pytest.raises(ValueError): - check_sampling_input("wrong_name", 0.5, "dataset") - - -def test_fit_compute_full_process(X_df, y_series): - """ - Test. - """ - clf = DecisionTreeClassifier() - vol = TrainTestVolatility( - clf, scoring=["roc_auc", "recall"], iterations=3, sample_train_test_split_seed=False, random_state=42 - ) - - report = vol.fit_compute(X_df, y_series) - assert report.shape == (2, 6) - - # Check if plot runs - vol.plot(show=False) - - -@pytest.mark.skipif(os.environ.get("SKIP_LIGHTGBM") == "true", reason="LightGBM tests disabled") -def test_fit_compute_complex(complex_data, complex_lightgbm): - """ - Test. - """ - X, y = complex_data - vol = TrainTestVolatility( - complex_lightgbm, - scoring="roc_auc", - iterations=3, - sample_train_test_split_seed=True, - verbose=150, - random_state=42, - ) - - report = vol.fit_compute(X, y) - assert report.shape == (1, 6) - - # Check if plot runs - vol.plot(show=False) diff --git a/tests/missing_values/test_imputation.py b/tests/missing_values/test_imputation.py deleted file mode 100644 index a0300e8a..00000000 --- a/tests/missing_values/test_imputation.py +++ /dev/null @@ -1,107 +0,0 @@ -# Code to test the imputation strategies. -import os - -import numpy as np -import pandas as pd -import pytest -from sklearn.ensemble import RandomForestClassifier -from sklearn.experimental import enable_iterative_imputer # noqa -from sklearn.impute import IterativeImputer, KNNImputer, SimpleImputer -from sklearn.linear_model import LogisticRegression - -from probatus.missing_values.imputation import ImputationSelector - - -@pytest.fixture(scope="function") -def X(): - """ - Fixture. - """ - return pd.DataFrame( - { - "col_1": [1, np.nan, 1, 1, np.nan, 1, 1, 0, 1, 1], - "col_2": [0, 0, 0, np.nan, 0, 0, 0, 1, 0, 0], - "col_3": [1, 0, np.nan, 0, 1, np.nan, 1, 0, 1, 1], - "col_4": ["A", "B", "A", np.nan, "B", np.nan, "C", "A", "B", "C"], - }, - index=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - ) - - -@pytest.fixture(scope="function") -def y(): - """ - Fixture. - """ - return pd.Series([1, 0, 1, 0, 1, 0, 1, 0, 0, 0], index=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) - - -@pytest.fixture(scope="function") -def strategies(): - """ - Test strategies. - """ - return { - "Simple Median Imputer": SimpleImputer(strategy="median", add_indicator=True), - "Simple Mean Imputer": SimpleImputer(strategy="mean", add_indicator=True), - "Iterative Imputer": IterativeImputer(add_indicator=True, n_nearest_features=5, sample_posterior=True), - "KNN": KNNImputer(n_neighbors=3), - } - - -def test_imputation_linear(X, y, strategies, capsys): - """ - Test imputation linear. - """ - # Initialize the classifier - clf = LogisticRegression() - cmp = ImputationSelector(clf=clf, strategies=strategies, cv=3, model_na_support=False) - report = cmp.fit_compute(X, y) - _ = cmp.plot(show=False) - - assert cmp.fitted - cmp._check_if_fitted() - assert report.shape[0] == 4 - - # Check if there is any prints - out, _ = capsys.readouterr() - assert len(out) == 0 - - -def test_imputation_bagging(X, y, strategies, capsys): - """ - Test bagging. - """ - # Initialize the classifier - clf = RandomForestClassifier() - cmp = ImputationSelector(clf=clf, strategies=strategies, cv=3, model_na_support=False) - report = cmp.fit_compute(X, y) - _ = cmp.plot(show=False) - - assert cmp.fitted - cmp._check_if_fitted() - assert report.shape[0] == 4 - - # Check if there is any prints - out, _ = capsys.readouterr() - assert len(out) == 0 - - -@pytest.mark.skipif(os.environ.get("SKIP_LIGHTGBM") == "true", reason="LightGBM tests disabled") -def test_imputation_boosting(X, y, strategies, complex_lightgbm, capsys): - """ - Test boosting. - """ - # Initialize the classifier - clf = complex_lightgbm - cmp = ImputationSelector(clf=clf, strategies=strategies, cv=3, model_na_support=True) - report = cmp.fit_compute(X, y) - _ = cmp.plot(show=False) - - assert cmp.fitted - cmp._check_if_fitted() - assert report.shape[0] == 5 - - # Check if there is any prints - out, _ = capsys.readouterr() - assert len(out) == 0 diff --git a/tests/stat_tests/__init__.py b/tests/stat_tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/stat_tests/test_distribution_statistics.py b/tests/stat_tests/test_distribution_statistics.py deleted file mode 100644 index 86d69bb9..00000000 --- a/tests/stat_tests/test_distribution_statistics.py +++ /dev/null @@ -1,244 +0,0 @@ -import numbers - -import numpy as np -import pandas as pd -import pytest -from sklearn.datasets import make_classification -from sklearn.model_selection import train_test_split - -from probatus.stat_tests import AutoDist, DistributionStatistics, ks, psi - - -def test_distribution_statistics_base(): - """ - Test. - """ - with pytest.raises(NotImplementedError): - assert DistributionStatistics("doesnotexist", "SimpleBucketer", bin_count=10) - with pytest.raises(NotImplementedError): - assert DistributionStatistics("psi", "doesnotexist", bin_count=10) - myTest = DistributionStatistics("psi", "SimpleBucketer", bin_count=10) - assert repr(myTest).startswith("DistributionStatistics") - - -def test_distribution_statistics_psi(): - """ - Test. - """ - d1 = np.histogram(np.random.normal(size=1000), 10)[0] - d2 = np.histogram(np.random.weibull(1, size=1000) - 1, 10)[0] - myTest = DistributionStatistics("psi", "SimpleBucketer", bin_count=10) - assert not myTest.fitted - psi_test, p_value_test = myTest.compute(d1, d2) - assert myTest.fitted - assert isinstance(psi_test, numbers.Number) - - -def test_distribution_statistics_tuple_output(): - """ - Test. - """ - d1 = np.histogram(np.random.normal(size=1000), 10)[0] - d2 = np.histogram(np.random.weibull(1, size=1000) - 1, 10)[0] - myTest = DistributionStatistics("ks", "SimpleBucketer", bin_count=10) - assert not myTest.fitted - res = myTest.compute(d1, d2) - assert myTest.fitted - assert isinstance(res, tuple) - - -def test_distribution_statistics_ks_no_binning(): - """ - Test. - """ - d1 = np.histogram(np.random.normal(size=1000), 10)[0] - d2 = np.histogram(np.random.weibull(1, size=1000) - 1, 10)[0] - myTest = DistributionStatistics("ks", binning_strategy=None) - assert not myTest.fitted - res = myTest.compute(d1, d2) - assert myTest.fitted - assert isinstance(res, tuple) - - -def test_distribution_statistics_attributes_psi(): - """ - Test. - """ - a = np.random.normal(size=1000) - b = np.random.normal(size=1000) - d1 = np.histogram(a, 10)[0] - d2 = np.histogram(b, 10)[0] - myTest = DistributionStatistics("psi", binning_strategy=None) - _ = myTest.compute(d1, d2, verbose=False) - psi_value_test, p_value_test = psi(d1, d2, verbose=False) - assert myTest.statistic == psi_value_test - - -def test_distribution_statistics_attributes_ks(): - """ - Test. - """ - d1 = np.histogram(np.random.normal(size=1000), 10)[0] - d2 = np.histogram(np.random.normal(size=1000), 10)[0] - myTest = DistributionStatistics("ks", binning_strategy=None) - _ = myTest.compute(d1, d2, verbose=False) - ks_value, p_value = ks(d1, d2) - assert myTest.statistic == ks_value - - -def test_distribution_statistics_autodist_base(): - """ - Test. - """ - nr_features = 2 - size = 1000 - np.random.seed(0) - df1 = pd.DataFrame(np.random.normal(size=(size, nr_features)), columns=[f"feat_{x}" for x in range(nr_features)]) - df2 = pd.DataFrame(np.random.normal(size=(size, nr_features)), columns=[f"feat_{x}" for x in range(nr_features)]) - features = df1.columns - myAutoDist = AutoDist(statistical_tests="all", binning_strategies="all", bin_count=[10, 20]) - assert repr(myAutoDist).startswith("AutoDist") - assert not myAutoDist.fitted - res = myAutoDist.compute(df1, df2, column_names=features) - assert myAutoDist.fitted - pd.testing.assert_frame_equal(res, myAutoDist.result) - assert isinstance(res, pd.DataFrame) - assert res["column"].values.tolist() == features.to_list() - - dist = DistributionStatistics(statistical_test="ks", binning_strategy="simplebucketer", bin_count=10) - dist.compute(df1["feat_0"], df2["feat_0"]) - assert dist.p_value == res.loc[res["column"] == "feat_0", "p_value_KS_simplebucketer_10"][0] - assert dist.statistic == res.loc[res["column"] == "feat_0", "statistic_KS_simplebucketer_10"][0] - - dist = DistributionStatistics(statistical_test="ks", binning_strategy=None, bin_count=10) - dist.compute(df1["feat_0"], df2["feat_0"]) - assert dist.p_value == res.loc[res["column"] == "feat_0", "p_value_KS_no_bucketing_0"][0] - assert dist.statistic == res.loc[res["column"] == "feat_0", "statistic_KS_no_bucketing_0"][0] - - -def test_distribution_statistics_autodist_column_names_error(): - """ - Test. - """ - df1 = pd.DataFrame({"feat_0": [1, 2, 3, 4, 5], "feat_1": [5, 6, 7, 8, 9]}) - df2 = df1 - features = df1.columns.values.tolist() + ["missing_feature"] - myAutoDist = AutoDist() - with pytest.raises(Exception): - assert myAutoDist.compute(df1, df2, column_names=features) - - df1 = pd.DataFrame({"feat_0": [1, 2, 3, 4, 5], "feat_1": [5, 6, 7, 8, 9]}) - df2 = df1.copy() - df1["feat_2"] = 0 - features = df2.columns.values.tolist() + ["missing_feature"] - myAutoDist = AutoDist() - with pytest.raises(Exception): - assert myAutoDist.compute(df1, df2, column_names=features) - - -@pytest.mark.skip(reason="Currently fails on ubuntu, to be investigated further.") -def test_distribution_statistics_autodist_return_failed_tests(): - """ - Test. - """ - df1 = pd.DataFrame({"feat_0": [1, 2, 3, 4, 5], "feat_1": [5, 6, 7, 8, 9]}) - df2 = df1 - features = df1.columns.values.tolist() - myAutoDist = AutoDist(binning_strategies="all") - res = myAutoDist.compute(df1, df2, column_names=features, return_failed_tests=True) - assert res.isin(["an error occurred"]).any().any() - res = myAutoDist.compute(df1, df2, column_names=features, return_failed_tests=False) - assert not res.isin(["an error occurred"]).any().any() - - -def test_distribution_statistics_autodist_default(): - """ - Test. - """ - df1 = pd.DataFrame({"feat_0": [1, 2, 3, 4, 5], "feat_1": [5, 6, 7, 8, 9]}) - df2 = df1 - features = df1.columns.values.tolist() - myAutoDist = AutoDist(binning_strategies="default", bin_count=10) - res = myAutoDist.compute(df1, df2, column_names=features) - for stat_test, stat_info in DistributionStatistics.statistical_test_dict.items(): - if stat_info["default_binning"]: - assert f"p_value_{stat_test}_{stat_info['default_binning']}_10" in res.columns - else: - assert f"p_value_{stat_test}_no_bucketing_0" in res.columns - - assert "p_value_agglomerativebucketer_10" not in res.columns - assert res.shape == (len(df1.columns), 1 + 2 * len(DistributionStatistics.statistical_test_dict)) - - -def test_distribution_statistics_autodist_init(): - """ - Test. - """ - myAutoDist = AutoDist(statistical_tests="all", binning_strategies="all") - assert isinstance(myAutoDist.statistical_tests, list) - myAutoDist = AutoDist(statistical_tests="ks", binning_strategies="all") - assert myAutoDist.statistical_tests == ["ks"] - myAutoDist = AutoDist(statistical_tests=["ks", "psi"], binning_strategies="all") - assert myAutoDist.statistical_tests == ["ks", "psi"] - - myAutoDist = AutoDist(statistical_tests="all", binning_strategies="all") - assert isinstance(myAutoDist.binning_strategies, list) - myAutoDist = AutoDist(statistical_tests="all", binning_strategies="quantilebucketer") - assert myAutoDist.binning_strategies == ["quantilebucketer"] - myAutoDist = AutoDist(statistical_tests="all", binning_strategies=["quantilebucketer", "simplebucketer"]) - assert myAutoDist.binning_strategies == ["quantilebucketer", "simplebucketer"] - - -def test_missing_values_in_autodist(): - """Test missing values have no impact in AutoDist functionality.""" - # Create dummy dataframe - X, y = make_classification(50, 5, random_state=0) - X = pd.DataFrame(X) - # Split train and test - X_train, X_test, _, _ = train_test_split(X, y, test_size=0.2, random_state=1) - # Define an add-on with only missing values - X_na = pd.DataFrame(np.tile(np.nan, (X.shape[1], X.shape[1]))) - - # Compute the statistics with the missing values - with_missings = AutoDist( - statistical_tests=["PSI", "KS"], binning_strategies="SimpleBucketer", bin_count=10 - ).compute(pd.concat([X_train, X_na]), pd.concat([X_test, X_na])) - - # Compute the statistics withpout the missing values - no_missing = AutoDist(statistical_tests=["PSI", "KS"], binning_strategies="SimpleBucketer", bin_count=10).compute( - X_train, X_test - ) - - # Test the two set of results are identical - pd.testing.assert_frame_equal(with_missings, no_missing) - - -def test_warnings_are_issued_for_missing(): - """Test if warnings are issued when missing values are present in the input of autodist.""" - # Generate an input dataframe without missing values - X = pd.DataFrame({"A": [number for number in range(0, 50)]}) - X = X.assign(B=X["A"], C=X["A"], D=X["A"], E=X["A"]) - - # Add some missing values to the dataframe. - X_na = X.copy() - X_na.iloc[X.sample(5, random_state=1).index, 1:3] = np.nan - - # Test missing value removal on the first data input. - with pytest.warns(None) as record_first: - _ = AutoDist(statistical_tests=["PSI"], binning_strategies="SimpleBucketer", bin_count=10).compute(X_na, X) - assert len(record_first) == 2 - - # Test missing values removal on the second data input - with pytest.warns(None) as record_second: - _ = AutoDist(statistical_tests=["PSI"], binning_strategies="SimpleBucketer", bin_count=10).compute(X, X_na) - assert len(record_second) == 2 - - # Test the missing values removal on the first and second data input - with pytest.warns(None) as record_both: - _ = AutoDist(statistical_tests=["PSI"], binning_strategies="SimpleBucketer", bin_count=10).compute(X_na, X_na) - assert len(record_both) == 2 - - # Test case where there are no missing values - with pytest.warns(None) as record_both: - _ = AutoDist(statistical_tests=["PSI"], binning_strategies="SimpleBucketer", bin_count=10).compute(X, X) - assert len(record_both) == 0 diff --git a/tests/stat_tests/test_stat_tests.py b/tests/stat_tests/test_stat_tests.py deleted file mode 100644 index a62f1a1c..00000000 --- a/tests/stat_tests/test_stat_tests.py +++ /dev/null @@ -1,100 +0,0 @@ -import numpy as np -import pandas as pd - -from probatus.binning import binning -from probatus.stat_tests import ad, es, ks, psi, sw - - -def test_psi_returns_zero(): - """ - Test. - """ - x = np.random.normal(size=1000) - myBucketer = binning.QuantileBucketer(bin_count=10) - myBucketer.fit(x) - d1 = myBucketer.counts_ - d2 = d1 - psi_test, p_value_test = psi(d1, d2, verbose=False) - assert psi_test == 0.0 - - -def test_psi_returns_large(): - """ - Test. - """ - d1 = np.histogram(np.random.normal(size=1000), 10)[0] - d2 = np.histogram(np.random.weibull(1, size=1000) - 1, 10)[0] - psi_test, p_value_test = psi(d1, d2, verbose=False) - assert psi_test > 1.0 - - -def test_ks_returns_one(): - """ - Test. - """ - d1 = np.random.normal(size=1000) - d2 = d1 - assert ks(d1, d2)[1] == 1.0 - - -def test_ks_accepts_pd_series(): - """ - Test. - """ - d1 = pd.Series(np.random.normal(size=1000)) - d2 = d1 - assert ks(d1, d2)[1] == 1.0 - - -def test_ks_returns_small(): - """ - Test. - """ - d1 = np.random.normal(size=1000) - d2 = np.random.weibull(1, size=1000) - 1 - assert ks(d1, d2)[1] < 0.001 - - -def test_es_returns_one(): - """ - Test. - """ - d1 = np.random.normal(size=1000) - d2 = d1 - assert es(d1, d2)[1] == 1.0 - - -def test_es_returns_small(): - """ - Test. - """ - d1 = np.random.normal(size=1000) - d2 = np.random.weibull(1, size=1000) - 1 - assert es(d1, d2)[1] < 0.001 - - -def test_ad_returns_big(): - """ - Test. - """ - d1 = np.random.normal(size=1000) - d2 = d1 - assert ad(d1, d2)[1] >= 0.25 - - -def test_ad_returns_small(): - """ - Test. - """ - d1 = np.random.normal(size=1000) - d2 = np.random.weibull(1, size=1000) - 1 - assert ad(d1, d2)[1] <= 0.001 - - -def test_sw_returns_zero(): - """ - Test. - """ - d1 = np.random.normal(size=1000) - d2 = d1 - assert sw(d1, d2)[0] == 0 diff --git a/tests/stat_tests/test_utils.py b/tests/stat_tests/test_utils.py deleted file mode 100644 index 58e8d1b8..00000000 --- a/tests/stat_tests/test_utils.py +++ /dev/null @@ -1,34 +0,0 @@ -import numpy as np - -from probatus.stat_tests import es, ks - - -def test_verbosity_true_(capsys): - """ - Test. - """ - d1 = np.random.normal(size=1000) - d2 = d1 - ks(d1, d2, verbose=True) - captured = capsys.readouterr() - assert ( - captured.out - == "\nKS: pvalue = 1.0\n\nKS: Null hypothesis cannot be rejected. Distributions not statistically different.\n" - ) - es(d1, d2, verbose=True) - captured = capsys.readouterr() - assert ( - captured.out - == "\nES: pvalue = 1.0\n\nES: Null hypothesis cannot be rejected. Distributions not statistically different.\n" - ) - - -def test_verbosity_false(capsys): - """ - Test. - """ - d1 = np.random.normal(size=1000) - d2 = d1 - ks(d1, d2, verbose=False) - captured = capsys.readouterr() - assert captured.out == ""